Merge pull request #777 from devLotto/feed-plugin

Feed plugin
This commit is contained in:
Adam
2018-03-04 09:36:16 -05:00
committed by GitHub
18 changed files with 1372 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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);
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> items;
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedResult> feed = Suppliers.memoizeWithExpiration(() ->
{
List<FeedItem> 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();
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> 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<FeedItem> 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());
}
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> 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<FeedItem> 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());
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> getTweets() throws IOException
{
return getTweets(false);
}
private List<FeedItem> 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<List<TwitterStatusesResponseItem>>()
{
}.getType();
List<TwitterStatusesResponseItem> statusesResponse = RuneLiteAPI.GSON
.fromJson(new InputStreamReader(in), listType);
List<FeedItem> 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;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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;
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -0,0 +1,358 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedItem> 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<FeedResult> feedSupplier;
FeedPanel(FeedConfig config, Supplier<FeedResult> 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("<html>");
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("<br>");
line = "";
lines++;
}
else
{
line = newLine;
pos++;
}
}
newText.append(line);
newText.append("</html>");
return newText.toString();
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (c) 2018, Lotto <https://github.com/devLotto>
* 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<FeedResult> 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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB