Created
February 7, 2018 23:13
-
-
Save jagrosh/21f69054b407c0f5eff582554127fd7c to your computer and use it in GitHub Desktop.
Discord Status API snippet, part of the bot that runs https://discord.gg/jn7TAP8
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2018 John Grosh <[email protected]>. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package jagbot.status; | |
import java.awt.Color; | |
import net.dv8tion.jda.core.entities.Game; | |
import net.dv8tion.jda.core.entities.Guild; | |
import net.dv8tion.jda.core.entities.Member; | |
import net.dv8tion.jda.core.entities.Role; | |
/** | |
* | |
* @author John Grosh ([email protected]) | |
*/ | |
public enum StatusEnum | |
{ | |
STREAMING(406706951846625280L, "406714314435854346", "#593695"), | |
ONLINE(406706905696960514L, "406714314368745472", "#43b581"), | |
IDLE(406706706978963457L, "406714314289053696", "#faa61a"), | |
DND(406706557867524098L, "406714313940664322", "#f04747"), | |
OFFLINE(0L, "406714314196647937", "#000000"); | |
private final long roleId; | |
private final String emoteId; | |
public final Color color; | |
private StatusEnum(long roleId, String emote, String color) | |
{ | |
this.roleId = roleId; | |
this.emoteId = emote; | |
this.color = Color.decode(color); | |
} | |
public Role getRole(Guild guild) | |
{ | |
return guild.getRoleById(roleId); | |
} | |
public String getEmoteLink() | |
{ | |
return "https://cdn.discordapp.com/emojis/"+emoteId+".png"; | |
} | |
public String getEmote() | |
{ | |
return "<:c:"+emoteId+">"; | |
} | |
public static StatusEnum from(Member member) | |
{ | |
if(member.getGame()!=null && member.getGame().getType()==Game.GameType.STREAMING) | |
return STREAMING; | |
switch(member.getOnlineStatus()) | |
{ | |
case ONLINE: return ONLINE; | |
case IDLE: return IDLE; | |
case DO_NOT_DISTURB: return DND; | |
default: return OFFLINE; | |
} | |
} | |
public static StatusEnum fromIndicator(String indicator) | |
{ | |
switch(indicator) | |
{ | |
case "none": return ONLINE; | |
case "minor": return IDLE; | |
case "major": return DND; | |
case "critical": return OFFLINE; | |
default: return ONLINE; | |
} | |
} | |
public static StatusEnum fromComponent(String component) | |
{ | |
switch(component) | |
{ | |
case "operational": return ONLINE; | |
case "degraded_performance": return IDLE; | |
case "partial_outage": return DND; | |
case "major_outage": return OFFLINE; | |
default: return ONLINE; | |
} | |
} | |
public static StatusEnum fromIncident(String incident) | |
{ | |
switch(incident.toLowerCase()) | |
{ | |
case "investigating": return DND; | |
case "identified": return IDLE; | |
case "monitoring": return IDLE; | |
case "resolved": return ONLINE; | |
case "postmortem": return STREAMING; | |
default: return STREAMING; | |
} | |
} | |
public static String textFormat(String text) | |
{ | |
return text.substring(0, 1).toUpperCase()+text.replace("_", " ").substring(1); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2018 John Grosh <[email protected]>. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package jagbot.status; | |
import java.io.IOException; | |
import java.io.Reader; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
import java.time.OffsetDateTime; | |
import java.time.temporal.ChronoUnit; | |
import java.util.LinkedList; | |
import java.util.List; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.ScheduledExecutorService; | |
import java.util.concurrent.TimeUnit; | |
import net.dv8tion.jda.core.EmbedBuilder; | |
import net.dv8tion.jda.core.entities.MessageEmbed; | |
import net.dv8tion.jda.webhook.WebhookClient; | |
import net.dv8tion.jda.webhook.WebhookClientBuilder; | |
import okhttp3.OkHttpClient; | |
import okhttp3.Request; | |
import org.json.JSONArray; | |
import org.json.JSONObject; | |
import org.json.JSONTokener; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* | |
* @author John Grosh ([email protected]) | |
*/ | |
public class StatuspageAPI | |
{ | |
private final static String STATUS_URL = "https://srhpyqt94yxb.statuspage.io/api/v2/summary.json"; | |
private final static String INCIDENTS_URL = "https://srhpyqt94yxb.statuspage.io/api/v2/incidents.json"; | |
private final static String CLICKABLE_INCIDENT = "https://status.discordapp.com/incidents/"; | |
private final static String WEBHOOK_URL = "https://canary.discordapp.com/api/webhooks/406773511290486795/CENSORED"; | |
private final static String LATEST_FILE = "latest_status.txt"; | |
private final static int LATEST_MAX_SIZE = 100; | |
private final static Logger LOG = LoggerFactory.getLogger("StatusPage"); | |
private final ScheduledExecutorService threadpool; | |
private final OkHttpClient client; | |
private final WebhookClient webhook; | |
private final List<String> latestIncidents = new LinkedList<>(); | |
public StatuspageAPI() | |
{ | |
threadpool = Executors.newSingleThreadScheduledExecutor(); | |
client = new OkHttpClient.Builder().build(); | |
webhook = new WebhookClientBuilder(WEBHOOK_URL).setHttpClient(client).build(); | |
try | |
{ | |
latestIncidents.addAll(Files.readAllLines(Paths.get(LATEST_FILE))); | |
while(latestIncidents.size()>LATEST_MAX_SIZE) | |
latestIncidents.remove(0); | |
} | |
catch(Exception e) | |
{ | |
LOG.warn("Failed to read latest incidents: "+e); | |
} | |
threadpool.scheduleWithFixedDelay(()->readIncidents(), 0, 10, TimeUnit.MINUTES); | |
} | |
public DiscordStatus getStatus() | |
{ | |
try(Reader reader = client.newCall(new Request.Builder() | |
.get().url(STATUS_URL) | |
.header("Content-Type", "application/json") | |
.build()).execute().body().charStream()) | |
{ | |
JSONObject obj = new JSONObject(new JSONTokener(reader)); | |
JSONObject status = obj.getJSONObject("status"); | |
JSONObject page = obj.getJSONObject("page"); | |
JSONArray components = obj.getJSONArray("components"); | |
DiscordStatus ds = new DiscordStatus(status.getString("indicator"), status.getString("description"), page.getString("updated_at")); | |
JSONObject component; | |
for(int i=0; i<components.length(); i++) | |
{ | |
component = components.getJSONObject(i); | |
ds.addComponent(component.getString("name"), component.getString("status"), !component.isNull("group_id")); | |
} | |
JSONArray incidents = obj.getJSONArray("incidents"); | |
if(incidents.length()>0) | |
{ | |
JSONObject latestIncident = incidents.getJSONObject(incidents.length()-1); | |
JSONObject latestIncidentUpdate = latestIncident.getJSONArray("incident_updates").getJSONObject(latestIncident.getJSONArray("incident_updates").length()-1); | |
ds.setIncident(latestIncident.getString("name"), latestIncident.getString("id"), | |
latestIncidentUpdate.getString("status"), latestIncidentUpdate.getString("body"), latestIncidentUpdate.getString("created_at")); | |
handleIncidents(incidents); | |
} | |
return ds; | |
} catch (Exception e) | |
{ | |
LOG.error("Failed to read summary: ", e); | |
} | |
return null; | |
} | |
private void readIncidents() | |
{ | |
try(Reader reader = client.newCall(new Request.Builder() | |
.get().url(INCIDENTS_URL) | |
.header("Content-Type", "application/json") | |
.build()).execute().body().charStream()) | |
{ | |
JSONObject obj = new JSONObject(new JSONTokener(reader)); | |
handleIncidents(obj.getJSONArray("incidents")); | |
} catch (Exception e) | |
{ | |
LOG.error("Failed to read summary: ", e); | |
} | |
} | |
private void handleIncidents(JSONArray incidents) | |
{ | |
List<MessageEmbed> embeds = new LinkedList<>(); | |
for(int i=incidents.length()-1; i>=0; i--) | |
{ | |
JSONObject incident = incidents.getJSONObject(i); | |
JSONArray updates = incident.getJSONArray("incident_updates"); | |
for(int j=updates.length()-1; j>=0; j--) | |
{ | |
JSONObject update = updates.getJSONObject(j); | |
OffsetDateTime time = OffsetDateTime.parse(update.getString("created_at")); | |
if(time.until(OffsetDateTime.now(), ChronoUnit.DAYS)>3) | |
continue; | |
String uid = incident.getString("id")+"-"+update.getString("id"); | |
if(latestIncidents.contains(uid)) | |
continue; | |
latestIncidents.add(uid); | |
StatusEnum status = StatusEnum.fromIncident(update.getString("status")); | |
String body = update.getString("body"); | |
if(body.length()>2048) | |
body = body.substring(0,2040)+" (...)"; | |
embeds.add(new EmbedBuilder() | |
.setTitle(incident.getString("name"), CLICKABLE_INCIDENT+incident.getString("id")) | |
.setDescription(body) | |
.setColor(status.color) | |
.setFooter(StatusEnum.textFormat(update.getString("status")), status.getEmoteLink()) | |
.setTimestamp(time) | |
.build()); | |
} | |
} | |
if(!embeds.isEmpty()) | |
{ | |
while(latestIncidents.size()>LATEST_MAX_SIZE) | |
latestIncidents.remove(0); | |
StringBuilder sb = new StringBuilder(); | |
latestIncidents.forEach(i -> sb.append("\n").append(i)); | |
try | |
{ | |
Files.write(Paths.get(LATEST_FILE), sb.toString().trim().getBytes()); | |
} | |
catch(IOException ex) | |
{ | |
LOG.warn("Failed to write incidents: "+ex); | |
} | |
} | |
while(embeds.size()>5) | |
{ | |
webhook.send(embeds.subList(0, 5)); | |
embeds.subList(0, 5).clear(); | |
} | |
if(!embeds.isEmpty()) | |
webhook.send(embeds); | |
} | |
public class DiscordStatus | |
{ | |
public final StatusEnum status; | |
public final String statusDescription; | |
public final OffsetDateTime lastUpdate; | |
public final List<Component> components; | |
public Incident latestIncident; | |
private DiscordStatus(String indicator, String description, String lastUpdate) | |
{ | |
this.status = StatusEnum.fromIndicator(indicator); | |
this.statusDescription = description; | |
this.lastUpdate = OffsetDateTime.parse(lastUpdate); | |
this.components = new LinkedList<>(); | |
} | |
private void addComponent(String name, String status, boolean grouped) | |
{ | |
this.components.add(new Component(name, status, grouped)); | |
} | |
private void setIncident(String name, String id, String status, String body, String time) | |
{ | |
this.latestIncident = new Incident(name, id, status, body, time); | |
} | |
public class Incident | |
{ | |
public final String name; | |
public final String id; | |
public final String status; | |
public final String body; | |
public final OffsetDateTime time; | |
private Incident(String name, String id, String status, String body, String time) | |
{ | |
this.name = name; | |
this.id = id; | |
this.status = status; | |
this.body = body; | |
this.time = OffsetDateTime.parse(time); | |
} | |
public String getURL() | |
{ | |
return CLICKABLE_INCIDENT+id; | |
} | |
public StatusEnum getStatus() | |
{ | |
return StatusEnum.fromIncident(status); | |
} | |
} | |
public class Component | |
{ | |
public final String name; | |
public final String status; | |
public final boolean grouped; | |
private Component(String name, String status, boolean grouped) | |
{ | |
this.name = name; | |
this.status = status; | |
this.grouped = grouped; | |
} | |
public StatusEnum getStatus() | |
{ | |
return StatusEnum.fromComponent(status); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment