@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 |
Reference in New Issue
Block a user