gitea-irc-bot/source/app.d

434 lines
11 KiB
D

import std.stdio;
import birchwood;
import core.thread;
import vibe.d;
import std.json;
import std.string;
import std.conv : to;
import std.net.curl;
import gogga;
import std.exception;
import std.file;
import core.stdc.stdlib : exit;
import gogga;
private __gshared GoggaLogger logger;
static this()
{
logger = new GoggaLogger();
}
/**
* TODO list
*
* 1. Fix the stripping of bad characters like \r\n etc etc in messages
*/
private class IRCBot : Client
{
this(ConnectionInfo connInfo)
{
super(connInfo);
}
public override void onChannelMessage(Message msg, string, string)
{
// this.sendMessage("fok", "#tlang"BB);
// channelMessage("fok", "#tlang");
}
}
void commitHandler(HTTPServerRequest request, HTTPServerResponse response)
{
/* Reply data */
JSONValue replyJSON;
int replyCode = 200;
replyJSON["output"]="";
/* Channel to eventually send to */
string toChannel;
try
{
/* Extract the received JSON */
JSONValue json = parseJSON(request.json().toString());
writeln(json.toPrettyString());
/* Extract the commits (if any) */
JSONValue[] commits = json["commits"].array();
/**
* A tag push will have no commits,
* for now ignore those. Only react
* if we have at least one commit and
* then react to the first one listed.
*/
if(commits.length > 0)
{
/* Extract the commit */
JSONValue commitBlock = json["commits"].array()[0];
string commitMessage = strip(commitBlock["message"].str());
string commitURL = commitBlock["url"].str();
string commitID = commitBlock["id"].str();
JSONValue authorBlock = commitBlock["author"];
string authorName = authorBlock["name"].str();
string authorEmail = authorBlock["email"].str();
string repositoryName = json["repository"]["full_name"].str();
/* Extract JUST the repository's name */
toChannel = getRespectiveChannel(json["repository"]["name"].str());
string ircMessage = bold("["~repositoryName~"]")~setForeground(SimpleColor.GREEN)~" New commit "~resetForegroundBackground()~commitMessage~" ("~commitID~") by "~italics(authorName)~" ("~authorEmail~") ["~underline(commitURL)~"]";
ircBot.channelMessage(ircMessage, toChannel); //TODO: Add IRC error handling
/* Send message to NTFY server */
notifySH(ircMessage);
}
else
{
logger.warn("Ignoring /commit triggered but with empty commits");
}
}
catch(Exception e)
{
replyCode=500;
replyJSON["output"]=e.toString();
}
response.writeJsonBody(replyJSON, 200);
}
void issueHandler(HTTPServerRequest request, HTTPServerResponse response)
{
/* Reply data */
JSONValue replyJSON;
int replyCode = 200;
replyJSON["output"]="";
/* Channel to eventually send to */
string toChannel;
try
{
/* Extract the received JSON */
JSONValue json = parseJSON(request.json().toString());
writeln(json.toPrettyString());
//Extract the type of action
JSONValue issueBlock = json["issue"];
string issueTitle = issueBlock["title"].str();
string issueURL = issueBlock["url"].str();
long issueID = issueBlock["id"].integer();
string issueAction = json["action"].str();
string repositoryName = issueBlock["repository"]["full_name"].str();
/* Extract JUST the repository's name */
toChannel = getRespectiveChannel(json["repository"]["name"].str());
/* Opened a new issue */
if(cmp(issueAction, "opened") == 0)
{
JSONValue userBlock = issueBlock["user"];
string username = userBlock["username"].str();
//TODO: Add IRC error handling
string ircMessage = bold("["~repositoryName~"]")~setForeground(SimpleColor.GREEN)~" Opened issue"~resetForegroundBackground()~" '"~issueTitle~"' "~bold("#"~to!(string)(issueID))~" by "~italics(username)~" ["~underline(issueURL)~"]";
ircBot.channelMessage(ircMessage, toChannel);
/* Send message to NTFY server */
notifySH(ircMessage);
}
/* Closed an old issue */
else if(cmp(issueAction, "closed") == 0)
{
JSONValue userBlock = issueBlock["user"];
string username = userBlock["username"].str();
//TODO: Add IRC error handling
string ircMessage = bold("["~repositoryName~"]")~setForeground(SimpleColor.RED)~" Closed issue"~resetForegroundBackground()~" '"~issueTitle~"' on issue "~bold("#"~to!(string)(issueID))~" by "~italics(username)~" ["~underline(issueURL)~"]";
ircBot.channelMessage(ircMessage, toChannel);
/* Send message to NTFY server */
notifySH(ircMessage);
}
/* Reopened an old issue */
else if(cmp(issueAction, "reopened") == 0)
{
JSONValue userBlock = issueBlock["user"];
string username = userBlock["username"].str();
//TODO: Add IRC error handling
string ircMessage = bold("["~repositoryName~"]")~setForeground(SimpleColor.GREEN)~" Reopened issue"~resetForegroundBackground()~" '"~issueTitle~"' "~bold("#"~to!(string)(issueID))~" by "~italics(username)~" ["~underline(issueURL)~"]";
ircBot.channelMessage(ircMessage, toChannel);
/* Send message to NTFY server */
notifySH(ircMessage);
}
/* Added a comment */
else if(cmp(issueAction, "created") == 0)
{
JSONValue commentBlock = json["comment"];
string commentBody = commentBlock["body"].str();
ulong commentLen = commentBody.length;
if(!(commentLen <= 30))
{
commentBody=commentBody[0..31]~"...";
}
JSONValue userBlock = commentBlock["user"];
string username = userBlock["username"].str();
//TODO: Add IRC error handling
string ircMessage = bold("["~repositoryName~"]")~" "~setForeground(SimpleColor.GREEN)~"New comment"~resetForegroundBackground()~" '"~italics(commentBody)~"' by "~italics(username)~" on issue "~bold("#"~to!(string)(issueID))~" "~issueTitle~" ["~underline(issueURL)~"]";
ircBot.channelMessage(ircMessage, toChannel);
/* Send message to NTFY server */
notifySH(ircMessage);
}
}
catch(Exception e)
{
replyCode=500;
replyJSON["output"]=e.toString();
}
response.writeJsonBody(replyJSON, 200);
}
void pullRequestHandler(HTTPServerRequest request, HTTPServerResponse response)
{
/* Reply data */
JSONValue replyJSON;
int replyCode = 200;
replyJSON["output"]="";
try
{
/* Extract the received JSON */
JSONValue json = parseJSON(request.json().toString());
writeln(json.toPrettyString());
//TODO: Implement me
}
catch(Exception e)
{
replyCode=500;
replyJSON["output"]=e.toString();
}
response.writeJsonBody(replyJSON, 200);
}
/* IRC client */
//TODO: THis should have a lock on it (maybe shared which let's us automatically do it)
//such that we can have another thread replace it when a disconnect happens or just to reconnect it
//TODO: Birchwood handling of disconnects (check this)
IRCBot ircBot;
/* Configuration file */
JSONValue config;
string[] listenAddresses;
ushort listenPort;
bool hasNTFYSH = false;
string ntfyServer, ntfyChannel;
string serverHost;
ushort serverPort;
string nickname;
string[] channels;
string[string] associations;
/**
* Sends a message to ntfy.sh (only if it is enabled)
*
* Params:
* message = the message to send to ntfy.sh
*/
void notifySH(string message)
{
//TODO: Add support for fancier formatted NTFY.SH messages
if(hasNTFYSH)
{
logger.info("Sending message to ntfy.sh ...");
post(ntfyServer~"/"~ntfyChannel, message);
logger.info("Sending message to ntfy.sh ... [done]");
}
}
/**
* Given a repository's name this will look it
* up in the key-value store to find the respective
* channel that should be used to send the message to
*
* Params:
* repositoryName = the repository to lookup by
* Returns: the channel's name
* Throws:
* Exception = if the repository does not exist
* in the map
*/
private string getRespectiveChannel(string repositoryName)
{
string* channelName = repositoryName in associations;
if(channelName is null)
{
throw new Exception("No channel exists for repository '"~repositoryName~"'");
}
return *channelName;
}
void main()
{
string configFilePath;
import std.process : environment;
/* If given an environment variable then use it as the configuration file */
if(environment.get("GIB_CONFIG") !is null)
{
/* Configuration file path */
configFilePath = environment.get("GIB_CONFIG");
}
/* If there is no environment variable, assume default config.json file */
else
{
/* Set to the default config path */
configFilePath = "config.json";
}
try
{
File configFile;
configFile.open(configFilePath);
ubyte[] configData;
configData.length = configFile.size();
configData = configFile.rawRead(configData);
configFile.close();
/* Parse the configuration */
config = parseJSON(cast(string)configData);
/* Web hook server details */
JSONValue webhookBlock = config["webhook"];
JSONValue listenBlock = webhookBlock["listen"];
JSONValue[] listenAddressesJSON = listenBlock["addresses"].array();
foreach(JSONValue listenAddress; listenAddressesJSON)
{
/* Get the listening address */
string listenAddressStr = listenAddress.str();
listenAddresses~=listenAddressStr;
}
listenPort = cast(ushort)(listenBlock["port"].integer());
/* IRC server details */
JSONValue ircBlock = config["irc"];
serverHost = ircBlock["host"].str();
serverPort = cast(ushort)(ircBlock["port"].integer());
nickname = ircBlock["nickname"].str();
/**
* Mapping between `repo -> #channel`
*
* Extract from the JSON, build the map
* and also construct a list of channels
* 9which we will use later to join
*/
JSONValue[string] channelAssociations = ircBlock["channels"].object();
foreach(string repoName; channelAssociations.keys())
{
associations[repoName] = channelAssociations[repoName].str();
channels ~= associations[repoName];
}
/* Attempt to parse ntfy.sh configuration */
try
{
JSONValue configNTFY = config["ntfy"];
ntfyServer = configNTFY["endpoint"].str();
ntfyChannel = configNTFY["topic"].str();
hasNTFYSH = true;
}
catch(JSONException e)
{
logger.warn("Not configuring NTFY as config is partially broken:\n\n"~e.msg);
}
logger.info("Your configuration is: \n"~config.toPrettyString());
}
catch(JSONException e)
{
logger.error("There was an error whilst parsing the config file:\n\n"~e.msg);
exit(-1);
}
catch(ErrnoException e)
{
logger.error("There was a problem opening the configuration file: "~e.msg);
exit(-1);
}
/* Configure IRC client */
ConnectionInfo connInfo = ConnectionInfo.newConnection(serverHost, serverPort, nickname, "tbot", "TLang Bot");
/* Set fakelag to none */
connInfo.setFakeLag(0);
/* Create a new IRC bot instance */
ircBot = new IRCBot(connInfo);
/* Connect to the server */
ircBot.connect();
/* Choose a nickname */
Thread.sleep(dur!("seconds")(2));
ircBot.command(new Message("", "NICK", nickname));
/* Identify oneself */
Thread.sleep(dur!("seconds")(2));
//TODO: Clean this string up
ircBot.command(new Message("", "USER", "giteabotweb giteabotweb irc.frdeenode.net :Tristan B. Kildaire"));
/* Join the requested channels */
Thread.sleep(dur!("seconds")(4));
ircBot.joinChannel(channels);
/* Setup the web server */
HTTPServerSettings httpServerSettings = new HTTPServerSettings();
httpServerSettings.port = listenPort;
httpServerSettings.bindAddresses = listenAddresses;
/* Create a router and add the supported routes */
URLRouter router = new URLRouter();
router.post("/commit", &commitHandler);
router.post("/issue", &issueHandler);
router.post("/pullrequest", &pullRequestHandler);
/* Attach the router to the HTTP server settings */
listenHTTP(httpServerSettings, router);
/* Starts the vibe-d event engine web server on the main thread */
runApplication();
}