Skip to content

Instantly share code, notes, and snippets.

@jagrosh
Created February 7, 2018 23:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jagrosh/21f69054b407c0f5eff582554127fd7c to your computer and use it in GitHub Desktop.
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
/*
* Copyright 2018 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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 (john.a.grosh@gmail.com)
*/
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);
}
}
/*
* Copyright 2018 John Grosh <john.a.grosh@gmail.com>.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package 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 (john.a.grosh@gmail.com)
*/
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