diff --git a/http-api/src/main/java/net/runelite/http/api/feed/FeedClient.java b/http-api/src/main/java/net/runelite/http/api/feed/FeedClient.java new file mode 100644 index 0000000000..a1f54cac82 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/feed/FeedClient.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.feed; + +import com.google.gson.JsonParseException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import net.runelite.http.api.RuneLiteAPI; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FeedClient +{ + private static final Logger logger = LoggerFactory.getLogger(FeedClient.class); + + public FeedResult lookupFeed() throws IOException + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("feed") + .build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + logger.debug("Error looking up feed: {}", response.message()); + return null; + } + + InputStream in = response.body().byteStream(); + return RuneLiteAPI.GSON.fromJson(new InputStreamReader(in), FeedResult.class); + } + catch (JsonParseException ex) + { + throw new IOException(ex); + } + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/feed/FeedItem.java b/http-api/src/main/java/net/runelite/http/api/feed/FeedItem.java new file mode 100644 index 0000000000..620f639d15 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/feed/FeedItem.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.feed; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +@AllArgsConstructor +public class FeedItem +{ + private final FeedItemType type; + private String avatar; + private final String title; + private final String content; + private final String url; + private final long timestamp; +} diff --git a/http-api/src/main/java/net/runelite/http/api/feed/FeedItemType.java b/http-api/src/main/java/net/runelite/http/api/feed/FeedItemType.java new file mode 100644 index 0000000000..e3cc9443cd --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/feed/FeedItemType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.feed; + +public enum FeedItemType +{ + BLOG_POST, + TWEET, + OSRS_NEWS +} diff --git a/http-api/src/main/java/net/runelite/http/api/feed/FeedResult.java b/http-api/src/main/java/net/runelite/http/api/feed/FeedResult.java new file mode 100644 index 0000000000..cf862b5be0 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/feed/FeedResult.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.feed; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class FeedResult +{ + private List items; +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/FeedController.java b/http-service/src/main/java/net/runelite/http/service/feed/FeedController.java new file mode 100644 index 0000000000..88e30c8eab --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/FeedController.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed; + +import com.google.common.base.Suppliers; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import net.runelite.http.api.feed.FeedResult; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.service.feed.blog.BlogService; +import net.runelite.http.service.feed.osrsnews.OSRSNewsService; +import net.runelite.http.service.feed.twitter.TwitterService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/feed") +public class FeedController +{ + private static final Logger logger = LoggerFactory.getLogger(FeedController.class); + + private BlogService blogService; + private TwitterService twitterService; + private OSRSNewsService osrsNewsService; + + private final Supplier feed = Suppliers.memoizeWithExpiration(() -> + { + List items = new ArrayList<>(); + + try + { + items.addAll(blogService.getBlogPosts()); + } + catch (IOException e) + { + logger.warn(null, e); + } + + try + { + items.addAll(twitterService.getTweets()); + } + catch (IOException e) + { + logger.warn(null, e); + } + + try + { + items.addAll(osrsNewsService.getNews()); + } + catch (IOException e) + { + logger.warn(null, e); + } + + return new FeedResult(items); + }, 10, TimeUnit.MINUTES); + + @Autowired + public FeedController(BlogService blogService, TwitterService twitterService, OSRSNewsService osrsNewsService) + { + this.blogService = blogService; + this.twitterService = twitterService; + this.osrsNewsService = osrsNewsService; + } + + @RequestMapping + public FeedResult getFeed() throws IOException + { + return feed.get(); + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/blog/BlogService.java b/http-service/src/main/java/net/runelite/http/service/feed/blog/BlogService.java new file mode 100644 index 0000000000..11ab1fd9a7 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/blog/BlogService.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed.blog; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Service; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +@Service +@Slf4j +public class BlogService +{ + private static final HttpUrl RSS_URL = HttpUrl.parse("https://runelite.net/atom.xml"); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + public List getBlogPosts() throws IOException + { + Request request = new Request.Builder() + .url(RSS_URL) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Error getting blog posts: " + response.message()); + } + + try + { + InputStream in = response.body().byteStream(); + Document document = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(in); + + Element documentElement = document.getDocumentElement(); + NodeList documentItems = documentElement.getElementsByTagName("entry"); + + List items = new ArrayList<>(); + + for (int i = 0; i < Math.min(documentItems.getLength(), 3); i++) + { + Node item = documentItems.item(i); + NodeList children = item.getChildNodes(); + + String title = null; + String summary = null; + String link = null; + long timestamp = -1; + + for (int j = 0; j < children.getLength(); j++) + { + Node childItem = children.item(j); + String nodeName = childItem.getNodeName(); + + switch (nodeName) + { + case "title": + title = childItem.getTextContent(); + break; + case "summary": + summary = childItem.getTextContent().replace("\n", "").trim(); + break; + case "link": + link = childItem.getAttributes().getNamedItem("href").getTextContent(); + break; + case "updated": + timestamp = DATE_FORMAT.parse(childItem.getTextContent()).getTime(); + break; + } + } + + if (title == null || summary == null || link == null || timestamp == -1) + { + throw new InternalServerErrorException("Failed to find title, summary, link and/or timestamp in the blog post feed"); + } + + items.add(new FeedItem(FeedItemType.BLOG_POST, title, summary, link, timestamp)); + } + + return items; + } + catch (ParserConfigurationException | SAXException | ParseException e) + { + throw new InternalServerErrorException("Failed to parse blog posts: " + e.getMessage()); + } + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/osrsnews/OSRSNewsService.java b/http-service/src/main/java/net/runelite/http/service/feed/osrsnews/OSRSNewsService.java new file mode 100644 index 0000000000..21298ab132 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/osrsnews/OSRSNewsService.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed.osrsnews; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Service; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +@Service +@Slf4j +public class OSRSNewsService +{ + private static final HttpUrl RSS_URL = HttpUrl.parse("http://services.runescape.com/m=news/latest_news.rss?oldschool=true"); + private static final SimpleDateFormat PUB_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy '00:00:00 GMT'", Locale.US); + + public List getNews() throws IOException + { + Request request = new Request.Builder() + .url(RSS_URL) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Error getting OSRS news: " + response.message()); + } + + try + { + InputStream in = response.body().byteStream(); + Document document = DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(in); + + Element documentElement = document.getDocumentElement(); + NodeList documentItems = documentElement.getElementsByTagName("item"); + + List items = new ArrayList<>(); + + for (int i = 0; i < documentItems.getLength(); i++) + { + Node item = documentItems.item(i); + NodeList children = item.getChildNodes(); + + String title = null; + String description = null; + String link = null; + long timestamp = -1; + + for (int j = 0; j < children.getLength(); j++) + { + Node childItem = children.item(j); + String nodeName = childItem.getNodeName(); + + switch (nodeName) + { + case "title": + title = childItem.getTextContent(); + break; + case "description": + description = childItem.getTextContent().replace("\n", "").trim(); + break; + case "link": + link = childItem.getTextContent(); + break; + case "pubDate": + timestamp = PUB_DATE_FORMAT.parse(childItem.getTextContent()).getTime(); + break; + } + } + + if (title == null || description == null || link == null || timestamp == -1) + { + throw new InternalServerErrorException("Failed to find title, description, link and/or timestamp in the OSRS RSS feed"); + } + + items.add(new FeedItem(FeedItemType.OSRS_NEWS, title, description, link, timestamp)); + } + + return items; + } + catch (ParserConfigurationException | SAXException | ParseException e) + { + throw new InternalServerErrorException("Failed to parse OSRS news: " + e.getMessage()); + } + } + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterOAuth2TokenResponse.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterOAuth2TokenResponse.java new file mode 100644 index 0000000000..24df0b6cf6 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterOAuth2TokenResponse.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed.twitter; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +@Data +class TwitterOAuth2TokenResponse +{ + @SerializedName("token_type") + private String tokenType; + + @SerializedName("access_token") + private String token; +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterService.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterService.java new file mode 100644 index 0000000000..74edfd0c2b --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterService.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed.twitter; + +import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.service.util.exception.InternalServerErrorException; +import okhttp3.FormBody; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class TwitterService +{ + private static final HttpUrl AUTH_URL = HttpUrl.parse("https://api.twitter.com/oauth2/token"); + private static final HttpUrl LIST_STATUSES_URL = HttpUrl.parse("https://api.twitter.com/1.1/lists/statuses.json"); + + private final String credentials; + private final String listId; + + private String token; + + @Autowired + public TwitterService( + @Value("${runelite.twitter.consumerkey}") String consumerKey, + @Value("${runelite.twitter.secretkey}") String consumerSecret, + @Value("${runelite.twitter.listid}") String listId + ) + { + this.credentials = consumerKey + ":" + consumerSecret; + this.listId = listId; + } + + public List getTweets() throws IOException + { + return getTweets(false); + } + + private List getTweets(boolean hasRetried) throws IOException + { + if (token == null) + { + updateToken(); + } + + HttpUrl url = LIST_STATUSES_URL.newBuilder() + .addQueryParameter("list_id", listId) + .addQueryParameter("count", "15") + .addQueryParameter("include_entities", "false") + .build(); + + Request request = new Request.Builder() + .url(url) + .header("Authorization", "Bearer " + token) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + switch (HttpStatus.valueOf(response.code())) + { + case BAD_REQUEST: + case UNAUTHORIZED: + updateToken(); + if (!hasRetried) + { + return getTweets(true); + } + throw new InternalServerErrorException("Could not auth to Twitter after trying once: " + response.message()); + default: + throw new IOException("Error getting Twitter list: " + response.message()); + } + } + + InputStream in = response.body().byteStream(); + Type listType = new TypeToken>() + { + }.getType(); + List statusesResponse = RuneLiteAPI.GSON + .fromJson(new InputStreamReader(in), listType); + + List items = new ArrayList<>(); + + for (TwitterStatusesResponseItem i : statusesResponse) + { + items.add(new FeedItem(FeedItemType.TWEET, + i.getUser().getProfileImageUrl(), + i.getUser().getScreenName(), + i.getText().replace("\n\n", " ").replaceAll("\n", " "), + "https://twitter.com/statuses/" + i.getId(), + getTimestampFromSnowflake(i.getId()))); + } + + return items; + } + } + + private void updateToken() throws IOException + { + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + + Request request = new Request.Builder() + .url(AUTH_URL) + .header("Authorization", "Basic " + encodedCredentials) + .post(new FormBody.Builder().add("grant_type", "client_credentials").build()) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + throw new IOException("Error authing to Twitter: " + response.message()); + } + + InputStream in = response.body().byteStream(); + TwitterOAuth2TokenResponse tokenResponse = RuneLiteAPI.GSON + .fromJson(new InputStreamReader(in), TwitterOAuth2TokenResponse.class); + + if (!tokenResponse.getTokenType().equals("bearer")) + { + throw new InternalServerErrorException("Returned token was not a bearer token"); + } + + if (tokenResponse.getToken() == null) + { + throw new InternalServerErrorException("Returned token was null"); + } + + token = tokenResponse.getToken(); + } + } + + /** + * Extracts the UTC timestamp from a Twitter snowflake as per + * https://github.com/client9/snowflake2time/blob/master/python/snowflake.py#L24 + */ + private long getTimestampFromSnowflake(long snowflake) + { + return (snowflake >> 22) + 1288834974657L; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItem.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItem.java new file mode 100644 index 0000000000..90b37c5021 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItem.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed.twitter; + +import lombok.Data; + +@Data +class TwitterStatusesResponseItem +{ + private long id; + private String text; + private TwitterStatusesResponseItemUser user; +} diff --git a/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItemUser.java b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItemUser.java new file mode 100644 index 0000000000..94fe9360f9 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/feed/twitter/TwitterStatusesResponseItemUser.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.feed.twitter; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +@Data +class TwitterStatusesResponseItemUser +{ + @SerializedName("screen_name") + private String screenName; + + @SerializedName("profile_image_url_https") + private String profileImageUrl; +} diff --git a/http-service/src/test/resources/application.properties b/http-service/src/test/resources/application.properties index 4d93d9bb41..62f152bff5 100644 --- a/http-service/src/test/resources/application.properties +++ b/http-service/src/test/resources/application.properties @@ -4,3 +4,6 @@ minio.endpoint=http://10.96.22.171:9000 minio.accesskey=AM54M27O4WZK65N6F8IP minio.secretkey=/PZCxzmsJzwCHYlogcymuprniGCaaLUOET2n6yMP minio.bucket=runelite +runelite.twitter.consumerkey=moo +runelite.twitter.secretkey=cow +runelite.twitter.listid=968949795153948673 diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedConfig.java new file mode 100644 index 0000000000..936a632a88 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedConfig.java @@ -0,0 +1,46 @@ +package net.runelite.client.plugins.feed; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "feed", + name = "News Feed", + description = "Displays client and game-related news" +) +public interface FeedConfig extends Config +{ + @ConfigItem( + keyName = "includeBlogPosts", + name = "Include Blog Posts", + description = "Configures whether blog posts are displayed", + position = 0 + ) + default boolean includeBlogPosts() + { + return true; + } + + @ConfigItem( + keyName = "includeTweets", + name = "Include Tweets", + description = "Configures whether tweets are displayed", + position = 1 + ) + default boolean includeTweets() + { + return true; + } + + @ConfigItem( + keyName = "includeOsrsNews", + name = "Include OSRS News", + description = "Configures whether OSRS news are displayed", + position = 2 + ) + default boolean includeOsrsNews() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedPanel.java new file mode 100644 index 0000000000..16472ec01d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedPanel.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.feed; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.font.FontRenderContext; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; +import java.util.function.Supplier; +import javax.imageio.ImageIO; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.feed.FeedItem; +import net.runelite.http.api.feed.FeedItemType; +import net.runelite.http.api.feed.FeedResult; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +@Slf4j +class FeedPanel extends PluginPanel +{ + private static BufferedImage RUNELITE_ICON; + private static BufferedImage OSRS_ICON; + + private static final Color TWEET_BACKGROUND = new Color(15, 15, 15); + private static final Color OSRS_NEWS_BACKGROUND = new Color(36, 30, 19); + private static final Color BLOG_POST_BACKGROUND = new Color(11, 30, 41); + + private static final int MAX_CONTENT_LINES = 3; + private static final int CONTENT_WIDTH = 148; + private static final int TIME_WIDTH = 20; + + private static final Comparator FEED_ITEM_COMPARATOR = (o1, o2) -> + { + if (o1.getType() != o2.getType()) + { + if (o1.getType() == FeedItemType.BLOG_POST) + { + return -1; + } + else if (o2.getType() == FeedItemType.BLOG_POST) + { + return 1; + } + } + + return -Long.compare(o1.getTimestamp(), o2.getTimestamp()); + }; + + static + { + try + { + RUNELITE_ICON = ImageIO.read(FeedPanel.class.getResourceAsStream("runelite.png")); + } + catch (IOException e) + { + log.warn("Client icon failed to load", e); + } + + try + { + OSRS_ICON = ImageIO.read(FeedPanel.class.getResourceAsStream("osrs.png")); + } + catch (IOException e) + { + log.warn("OSRS icon failed to load", e); + } + } + + private final FeedConfig config; + private final Supplier feedSupplier; + + FeedPanel(FeedConfig config, Supplier feedSupplier) + { + this.config = config; + this.feedSupplier = feedSupplier; + } + + void rebuildFeed() + { + FeedResult feed = feedSupplier.get(); + + if (feed == null) + { + return; + } + + SwingUtilities.invokeLater(() -> + { + removeAll(); + + feed.getItems() + .stream() + .filter(f -> f.getType() != FeedItemType.BLOG_POST || config.includeBlogPosts()) + .filter(f -> f.getType() != FeedItemType.TWEET || config.includeTweets()) + .filter(f -> f.getType() != FeedItemType.OSRS_NEWS || config.includeOsrsNews()) + .sorted(FEED_ITEM_COMPARATOR) + .forEach(this::addItemToPanel); + }); + } + + private void addItemToPanel(FeedItem item) + { + JPanel avatarAndRight = new JPanel(new BorderLayout()); + avatarAndRight.setPreferredSize(new Dimension(0, 56)); + + JLabel avatar = new JLabel(); + // width = 48+4 to compensate for the border + avatar.setPreferredSize(new Dimension(52, 48)); + avatar.setBorder(new EmptyBorder(0, 4, 0, 0)); + + switch (item.getType()) + { + case TWEET: + try + { + Request request = new Request.Builder() + .url(item.getAvatar()) + .build(); + + RuneLiteAPI.CLIENT.newCall(request).enqueue(new Callback() + { + @Override + public void onFailure(Call call, IOException e) + { + log.warn(null, e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException + { + try (ResponseBody responseBody = response.body()) + { + if (!response.isSuccessful()) + { + log.warn("Failed to download image " + item.getAvatar()); + return; + } + + avatar.setIcon(new ImageIcon(ImageIO.read(responseBody.byteStream()))); + } + } + }); + } + catch (IllegalArgumentException | NullPointerException e) + { + log.warn(null, e); + } + avatarAndRight.setBackground(TWEET_BACKGROUND); + break; + case OSRS_NEWS: + if (OSRS_ICON != null) + { + avatar.setIcon(new ImageIcon(OSRS_ICON)); + } + avatarAndRight.setBackground(OSRS_NEWS_BACKGROUND); + break; + default: + if (RUNELITE_ICON != null) + { + avatar.setIcon(new ImageIcon(RUNELITE_ICON)); + } + avatarAndRight.setBackground(BLOG_POST_BACKGROUND); + break; + } + + JPanel upAndContent = new JPanel(); + upAndContent.setLayout(new BoxLayout(upAndContent, BoxLayout.Y_AXIS)); + upAndContent.setBorder(new EmptyBorder(4, 8, 4, 4)); + upAndContent.setBackground(null); + + JPanel titleAndTime = new JPanel(); + titleAndTime.setLayout(new BorderLayout()); + titleAndTime.setBackground(null); + + Color darkerForeground = UIManager.getColor("Label.foreground").darker(); + + JLabel titleLabel = new JLabel(item.getTitle()); + titleLabel.setFont(FontManager.getRunescapeSmallFont()); + titleLabel.setBackground(null); + titleLabel.setForeground(darkerForeground); + titleLabel.setPreferredSize(new Dimension(CONTENT_WIDTH - TIME_WIDTH, 0)); + + Duration duration = Duration.between(Instant.ofEpochMilli(item.getTimestamp()), Instant.now()); + JLabel timeLabel = new JLabel(durationToString(duration)); + timeLabel.setFont(FontManager.getRunescapeSmallFont()); + timeLabel.setForeground(darkerForeground); + + titleAndTime.add(titleLabel, BorderLayout.WEST); + titleAndTime.add(timeLabel, BorderLayout.EAST); + + JPanel content = new JPanel(new BorderLayout()); + content.setBackground(null); + + JLabel contentLabel = new JLabel(lineBreakText(item.getContent(), FontManager.getRunescapeSmallFont())); + contentLabel.setBorder(new EmptyBorder(2, 0, 0, 0)); + contentLabel.setFont(FontManager.getRunescapeSmallFont()); + contentLabel.setForeground(darkerForeground); + + content.add(contentLabel, BorderLayout.CENTER); + + upAndContent.add(titleAndTime); + upAndContent.add(content); + upAndContent.add(new Box.Filler(new Dimension(0, 0), + new Dimension(0, Short.MAX_VALUE), + new Dimension(0, Short.MAX_VALUE))); + + avatarAndRight.add(avatar, BorderLayout.WEST); + avatarAndRight.add(upAndContent, BorderLayout.CENTER); + + Color backgroundColor = avatarAndRight.getBackground(); + Color hoverColor = backgroundColor.brighter().brighter(); + Color pressedColor = hoverColor.brighter(); + + avatarAndRight.addMouseListener(new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent e) + { + avatarAndRight.setBackground(hoverColor); + } + + @Override + public void mouseExited(MouseEvent e) + { + avatarAndRight.setBackground(backgroundColor); + } + + @Override + public void mousePressed(MouseEvent e) + { + avatarAndRight.setBackground(pressedColor); + } + + @Override + public void mouseReleased(MouseEvent e) + { + avatarAndRight.setBackground(hoverColor); + + Desktop desktop = Desktop.getDesktop(); + + if (!desktop.isSupported(Desktop.Action.BROWSE)) + { + log.info("Desktop browser is not supported"); + return; + } + + try + { + desktop.browse(new URI(item.getUrl())); + } + catch (IOException | URISyntaxException ex) + { + log.warn("Unable to open URL " + item.getUrl(), ex); + } + } + }); + + add(avatarAndRight); + } + + private String durationToString(Duration duration) + { + if (duration.getSeconds() >= 60 * 60 * 24) + { + return (int) (duration.getSeconds() / (60 * 60 * 24)) + "d"; + } + else if (duration.getSeconds() >= 60 * 60) + { + return (int) (duration.getSeconds() / (60 * 60)) + "h"; + } + return (int) (duration.getSeconds() / 60) + "m"; + } + + private String lineBreakText(String text, Font font) + { + StringBuilder newText = new StringBuilder(""); + + FontRenderContext fontRenderContext = new FontRenderContext(font.getTransform(), + true, true); + + int lines = 0; + int pos = 0; + String[] words = text.split(" "); + String line = ""; + + while (lines < MAX_CONTENT_LINES && pos < words.length) + { + String newLine = pos > 0 ? line + " " + words[pos] : words[pos]; + double width = font.getStringBounds(newLine, fontRenderContext).getWidth(); + + if (width >= CONTENT_WIDTH) + { + newText.append(line); + newText.append("
"); + line = ""; + lines++; + } + else + { + line = newLine; + pos++; + } + } + + newText.append(line); + newText.append(""); + + return newText.toString(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedPlugin.java new file mode 100644 index 0000000000..714ba106cd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/feed/FeedPlugin.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2018, Lotto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.feed; + +import com.google.common.base.Suppliers; +import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.events.ConfigChanged; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.task.Schedule; +import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.NavigationButton; +import net.runelite.http.api.feed.FeedClient; +import net.runelite.http.api.feed.FeedResult; + +@PluginDescriptor( + name = "News Feed", + loadWhenOutdated = true +) +@Slf4j +public class FeedPlugin extends Plugin +{ + @Inject + private ClientUI ui; + + @Inject + private FeedConfig config; + + @Inject + private ScheduledExecutorService executorService; + + private FeedPanel feedPanel; + private NavigationButton navButton; + + private FeedClient feedClient = new FeedClient(); + private Supplier feedSupplier = Suppliers.memoizeWithExpiration(() -> + { + try + { + return feedClient.lookupFeed(); + } + catch (IOException e) + { + log.warn(null, e); + } + return null; + }, 10, TimeUnit.MINUTES); + + @Override + protected void startUp() throws Exception + { + feedPanel = new FeedPanel(config, feedSupplier); + + navButton = new NavigationButton( + "News Feed", + ImageIO.read(getClass().getResourceAsStream("icon.png")), + () -> feedPanel); + + ui.getPluginToolbar().addNavigation(navButton); + + executorService.submit(this::updateFeed); + } + + @Override + protected void shutDown() throws Exception + { + ui.getPluginToolbar().removeNavigation(navButton); + } + + private void updateFeed() + { + feedPanel.rebuildFeed(); + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("feed")) + { + executorService.submit(this::updateFeed); + } + } + + @Schedule( + period = 10, + unit = ChronoUnit.MINUTES, + asynchronous = true + ) + private void updateFeedTask() + { + updateFeed(); + } + + @Provides + FeedConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(FeedConfig.class); + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/feed/icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/feed/icon.png new file mode 100644 index 0000000000..95b048b153 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/feed/icon.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/feed/osrs.png b/runelite-client/src/main/resources/net/runelite/client/plugins/feed/osrs.png new file mode 100644 index 0000000000..2039454a18 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/feed/osrs.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/feed/runelite.png b/runelite-client/src/main/resources/net/runelite/client/plugins/feed/runelite.png new file mode 100644 index 0000000000..a6c7fdcbae Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/feed/runelite.png differ