2022-11-06 10:09:15 +00:00
import std.stdio ;
import birchwood ;
import core.thread ;
import vibe.d ;
import std.json ;
import std.string ;
2022-11-07 14:24:23 +00:00
import std.conv : to ;
2022-11-13 18:31:50 +00:00
import std.net.curl ;
import gogga ;
import std.exception ;
2022-11-21 09:25:06 +00:00
import std.file ;
import core.stdc.stdlib : exit ;
2022-11-13 18:31:50 +00:00
2023-03-11 13:51:31 +00:00
import gogga ;
2023-03-19 13:22:14 +00:00
private __gshared GoggaLogger logger ;
2023-03-11 13:51:31 +00:00
static this ( )
{
logger = new GoggaLogger ( ) ;
}
2022-11-13 18:31:50 +00:00
/ * *
* TODO list
*
* 1. Fix the stripping of bad characters like \ r \ n etc etc in messages
* /
2022-11-06 10:09:15 +00:00
private class IRCBot : Client
{
this ( ConnectionInfo connInfo )
{
super ( connInfo ) ;
}
2022-11-08 08:19:25 +00:00
public override void onChannelMessage ( Message msg , string , string )
{
// this.sendMessage("fok", "#tlang"BB);
// channelMessage("fok", "#tlang");
}
2022-11-06 10:09:15 +00:00
}
void commitHandler ( HTTPServerRequest request , HTTPServerResponse response )
{
2022-11-08 08:19:25 +00:00
/* Reply data */
JSONValue replyJSON ;
int replyCode = 200 ;
replyJSON [ "output" ] = "" ;
2022-11-06 10:09:15 +00:00
2023-07-04 10:41:33 +01:00
/* Channel to eventually send to */
string toChannel ;
2022-11-08 08:19:25 +00:00
try
{
2022-11-13 18:31:50 +00:00
/* Extract the received JSON */
2022-11-08 08:19:25 +00:00
JSONValue json = parseJSON ( request . json ( ) . toString ( ) ) ;
writeln ( json . toPrettyString ( ) ) ;
2022-11-06 10:09:15 +00:00
2023-07-06 18:55:43 +01:00
/* 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 ( ) ;
2022-11-08 08:19:25 +00:00
2023-07-06 18:55:43 +01:00
/* Extract JUST the repository's name */
2023-09-24 14:47:32 +01:00
toChannel = getRespectiveChannel ( json [ "repository" ] [ "name" ] . str ( ) ) ;
2023-07-06 18:55:43 +01:00
2023-07-07 12:12:19 +01:00
string ircMessage = bold ( "[" ~ repositoryName ~ "]" ) ~ setForeground ( SimpleColor . GREEN ) ~ " New commit " ~ resetForegroundBackground ( ) ~ commitMessage ~ " (" ~ commitID ~ ") by " ~ italics ( authorName ) ~ " (" ~ authorEmail ~ ") [" ~ underline ( commitURL ) ~ "]" ;
2023-07-06 18:55:43 +01:00
ircBot . channelMessage ( ircMessage , toChannel ) ; //TODO: Add IRC error handling
/* Send message to NTFY server */
notifySH ( ircMessage ) ;
}
else
{
2023-07-06 18:56:10 +01:00
logger . warn ( "Ignoring /commit triggered but with empty commits" ) ;
2023-07-06 18:55:43 +01:00
}
2022-11-08 08:19:25 +00:00
}
catch ( Exception e )
{
replyCode = 500 ;
replyJSON [ "output" ] = e . toString ( ) ;
}
response . writeJsonBody ( replyJSON , 200 ) ;
2022-11-06 10:09:15 +00:00
}
void issueHandler ( HTTPServerRequest request , HTTPServerResponse response )
{
2022-11-08 08:19:25 +00:00
/* Reply data */
JSONValue replyJSON ;
int replyCode = 200 ;
replyJSON [ "output" ] = "" ;
2022-11-06 10:23:25 +00:00
2023-07-04 10:41:33 +01:00
/* Channel to eventually send to */
string toChannel ;
2022-11-08 08:19:25 +00:00
try
2022-11-07 14:24:23 +00:00
{
2022-11-13 18:31:50 +00:00
/* Extract the received JSON */
2022-11-08 08:19:25 +00:00
JSONValue json = parseJSON ( request . json ( ) . toString ( ) ) ;
writeln ( json . toPrettyString ( ) ) ;
2022-11-06 10:23:25 +00:00
2022-11-08 08:19:25 +00:00
//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 ( ) ;
2023-03-20 09:10:27 +00:00
string repositoryName = issueBlock [ "repository" ] [ "full_name" ] . str ( ) ;
2022-11-06 11:27:57 +00:00
2023-07-04 10:41:33 +01:00
/* Extract JUST the repository's name */
2023-09-24 14:47:32 +01:00
toChannel = getRespectiveChannel ( json [ "repository" ] [ "name" ] . str ( ) ) ;
2023-07-04 10:41:33 +01:00
2022-11-08 08:19:25 +00:00
/* Opened a new issue */
if ( cmp ( issueAction , "opened" ) = = 0 )
2022-11-06 11:27:57 +00:00
{
2022-11-08 08:19:25 +00:00
JSONValue userBlock = issueBlock [ "user" ] ;
string username = userBlock [ "username" ] . str ( ) ;
2022-11-13 18:31:50 +00:00
//TODO: Add IRC error handling
2023-06-13 13:56:39 +01:00
string ircMessage = bold ( "[" ~ repositoryName ~ "]" ) ~ setForeground ( SimpleColor . GREEN ) ~ " Opened issue" ~ resetForegroundBackground ( ) ~ " '" ~ issueTitle ~ "' " ~ bold ( "#" ~ to ! ( string ) ( issueID ) ) ~ " by " ~ italics ( username ) ~ " [" ~ underline ( issueURL ) ~ "]" ;
2023-07-04 10:41:33 +01:00
ircBot . channelMessage ( ircMessage , toChannel ) ;
2022-11-13 18:31:50 +00:00
/* Send message to NTFY server */
2022-11-14 08:52:05 +00:00
notifySH ( ircMessage ) ;
2022-11-06 11:27:57 +00:00
}
2022-11-08 08:19:25 +00:00
/* Closed an old issue */
else if ( cmp ( issueAction , "closed" ) = = 0 )
{
JSONValue userBlock = issueBlock [ "user" ] ;
string username = userBlock [ "username" ] . str ( ) ;
2022-11-13 18:31:50 +00:00
//TODO: Add IRC error handling
2023-06-12 19:30:17 +01:00
string ircMessage = bold ( "[" ~ repositoryName ~ "]" ) ~ setForeground ( SimpleColor . RED ) ~ " Closed issue" ~ resetForegroundBackground ( ) ~ " '" ~ issueTitle ~ "' on issue " ~ bold ( "#" ~ to ! ( string ) ( issueID ) ) ~ " by " ~ italics ( username ) ~ " [" ~ underline ( issueURL ) ~ "]" ;
2023-07-04 10:41:33 +01:00
ircBot . channelMessage ( ircMessage , toChannel ) ;
2022-11-13 18:31:50 +00:00
/* Send message to NTFY server */
2022-11-14 08:52:05 +00:00
notifySH ( ircMessage ) ;
2022-11-08 08:19:25 +00:00
}
/* Reopened an old issue */
else if ( cmp ( issueAction , "reopened" ) = = 0 )
{
JSONValue userBlock = issueBlock [ "user" ] ;
string username = userBlock [ "username" ] . str ( ) ;
2022-11-13 18:31:50 +00:00
//TODO: Add IRC error handling
2023-06-12 19:31:13 +01:00
string ircMessage = bold ( "[" ~ repositoryName ~ "]" ) ~ setForeground ( SimpleColor . GREEN ) ~ " Reopened issue" ~ resetForegroundBackground ( ) ~ " '" ~ issueTitle ~ "' " ~ bold ( "#" ~ to ! ( string ) ( issueID ) ) ~ " by " ~ italics ( username ) ~ " [" ~ underline ( issueURL ) ~ "]" ;
2023-07-04 10:41:33 +01:00
ircBot . channelMessage ( ircMessage , toChannel ) ;
2022-11-13 18:31:50 +00:00
/* Send message to NTFY server */
2022-11-14 08:52:05 +00:00
notifySH ( ircMessage ) ;
2022-11-08 08:19:25 +00:00
}
/* Added a comment */
else if ( cmp ( issueAction , "created" ) = = 0 )
{
JSONValue commentBlock = json [ "comment" ] ;
string commentBody = commentBlock [ "body" ] . str ( ) ;
ulong commentLen = commentBody . length ;
2022-11-06 11:27:57 +00:00
2022-11-08 08:19:25 +00:00
if ( ! ( commentLen < = 30 ) )
{
commentBody = commentBody [ 0. . 31 ] ~ "..." ;
}
2022-11-06 10:23:25 +00:00
2022-11-08 08:19:25 +00:00
JSONValue userBlock = commentBlock [ "user" ] ;
string username = userBlock [ "username" ] . str ( ) ;
2022-11-13 18:31:50 +00:00
//TODO: Add IRC error handling
2023-07-09 13:42:44 +01:00
string ircMessage = bold ( "[" ~ repositoryName ~ "]" ) ~ " " ~ setForeground ( SimpleColor . GREEN ) ~ "New comment" ~ resetForegroundBackground ( ) ~ " '" ~ italics ( commentBody ) ~ "' by " ~ italics ( username ) ~ " on issue " ~ bold ( "#" ~ to ! ( string ) ( issueID ) ) ~ " " ~ issueTitle ~ " [" ~ underline ( issueURL ) ~ "]" ;
2023-07-04 10:41:33 +01:00
ircBot . channelMessage ( ircMessage , toChannel ) ;
2022-11-13 18:31:50 +00:00
/* Send message to NTFY server */
2022-11-14 08:52:05 +00:00
notifySH ( ircMessage ) ;
2022-11-08 08:19:25 +00:00
}
2022-11-06 10:23:25 +00:00
}
2022-11-08 08:19:25 +00:00
catch ( Exception e )
{
replyCode = 500 ;
replyJSON [ "output" ] = e . toString ( ) ;
}
response . writeJsonBody ( replyJSON , 200 ) ;
}
2022-11-06 10:23:25 +00:00
2022-11-08 08:19:25 +00:00
void pullRequestHandler ( HTTPServerRequest request , HTTPServerResponse response )
{
/* Reply data */
JSONValue replyJSON ;
int replyCode = 200 ;
replyJSON [ "output" ] = "" ;
try
{
2022-11-13 18:31:50 +00:00
/* Extract the received JSON */
2022-11-08 08:19:25 +00:00
JSONValue json = parseJSON ( request . json ( ) . toString ( ) ) ;
writeln ( json . toPrettyString ( ) ) ;
2022-11-13 18:31:50 +00:00
//TODO: Implement me
2022-11-08 08:19:25 +00:00
}
catch ( Exception e )
{
replyCode = 500 ;
replyJSON [ "output" ] = e . toString ( ) ;
}
response . writeJsonBody ( replyJSON , 200 ) ;
2022-11-06 10:09:15 +00:00
}
2022-11-13 18:31:50 +00:00
/* IRC client */
2022-12-11 20:46:41 +00:00
//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)
2022-11-06 10:09:15 +00:00
IRCBot ircBot ;
2022-11-13 18:31:50 +00:00
/* Configuration file */
JSONValue config ;
2022-11-21 09:22:23 +00:00
string [ ] listenAddresses ;
ushort listenPort ;
2022-11-13 18:31:50 +00:00
bool hasNTFYSH = false ;
string ntfyServer , ntfyChannel ;
string serverHost ;
ushort serverPort ;
string nickname ;
2023-07-04 11:15:59 +01:00
string [ ] channels ;
2023-07-04 11:13:31 +01:00
string [ string ] associations ;
2022-11-13 18:31:50 +00:00
2022-11-14 08:52:05 +00:00
/ * *
* Sends a message to ntfy . sh ( only if it is enabled )
*
* Params :
* message = the message to send to ntfy . sh
* /
void notifySH ( string message )
{
2022-11-21 09:10:36 +00:00
//TODO: Add support for fancier formatted NTFY.SH messages
2022-11-14 08:52:05 +00:00
if ( hasNTFYSH )
{
2023-03-11 13:51:31 +00:00
logger . info ( "Sending message to ntfy.sh ..." ) ;
2022-11-14 08:52:05 +00:00
post ( ntfyServer ~ "/" ~ ntfyChannel , message ) ;
2023-03-11 13:51:31 +00:00
logger . info ( "Sending message to ntfy.sh ... [done]" ) ;
2022-11-14 08:52:05 +00:00
}
}
2023-09-24 14:46:28 +01:00
/ * *
* 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 ;
}
2023-07-04 17:08:20 +01:00
void main ( )
2022-11-06 10:09:15 +00:00
{
2022-11-13 18:31:50 +00:00
string configFilePath ;
2023-07-04 17:08:20 +01:00
import std.process : environment ;
/* If given an environment variable then use it as the configuration file */
if ( environment . get ( "GIB_CONFIG" ) ! is null )
2022-11-13 18:31:50 +00:00
{
/* Configuration file path */
2023-07-04 17:08:20 +01:00
configFilePath = environment . get ( "GIB_CONFIG" ) ;
2022-11-13 18:31:50 +00:00
}
2023-07-04 17:08:20 +01:00
/* If there is no environment variable, assume default config.json file */
2022-11-13 18:31:50 +00:00
else
{
/* Set to the default config path */
configFilePath = "config.json" ;
}
2023-07-04 17:08:20 +01:00
2022-11-13 18:31:50 +00:00
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 ) ;
2022-11-21 09:22:23 +00:00
/* 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 ( ) ) ;
2022-11-13 18:31:50 +00:00
/* IRC server details */
JSONValue ircBlock = config [ "irc" ] ;
serverHost = ircBlock [ "host" ] . str ( ) ;
serverPort = cast ( ushort ) ( ircBlock [ "port" ] . integer ( ) ) ;
nickname = ircBlock [ "nickname" ] . str ( ) ;
2023-07-04 11:13:31 +01:00
/ * *
* Mapping between `repo -> #channel`
2023-07-04 11:15:59 +01:00
*
* Extract from the JSON , build the map
* and also construct a list of channels
* 9 which we will use later to join
2023-07-04 11:13:31 +01:00
* /
JSONValue [ string ] channelAssociations = ircBlock [ "channels" ] . object ( ) ;
foreach ( string repoName ; channelAssociations . keys ( ) )
{
associations [ repoName ] = channelAssociations [ repoName ] . str ( ) ;
2023-07-04 11:15:59 +01:00
channels ~ = associations [ repoName ] ;
2023-07-04 11:13:31 +01:00
}
2022-11-13 18:31:50 +00:00
/* Attempt to parse ntfy.sh configuration */
try
{
JSONValue configNTFY = config [ "ntfy" ] ;
ntfyServer = configNTFY [ "endpoint" ] . str ( ) ;
ntfyChannel = configNTFY [ "topic" ] . str ( ) ;
hasNTFYSH = true ;
}
catch ( JSONException e )
{
2023-03-11 13:58:15 +00:00
logger . warn ( "Not configuring NTFY as config is partially broken:\n\n" ~ e . msg ) ;
2022-11-13 18:31:50 +00:00
}
2023-03-11 13:58:15 +00:00
logger . info ( "Your configuration is: \n" ~ config . toPrettyString ( ) ) ;
2022-11-13 18:31:50 +00:00
}
catch ( JSONException e )
{
2023-03-11 13:58:15 +00:00
logger . error ( "There was an error whilst parsing the config file:\n\n" ~ e . msg ) ;
2022-11-21 09:14:11 +00:00
exit ( - 1 ) ;
2022-11-13 18:31:50 +00:00
}
catch ( ErrnoException e )
{
2023-03-11 13:58:15 +00:00
logger . error ( "There was a problem opening the configuration file: " ~ e . msg ) ;
2022-11-21 09:14:11 +00:00
exit ( - 1 ) ;
2022-11-13 18:31:50 +00:00
}
2022-11-21 09:24:24 +00:00
/* Configure IRC client */
2023-06-12 19:30:17 +01:00
ConnectionInfo connInfo = ConnectionInfo . newConnection ( serverHost , serverPort , nickname , "tbot" , "TLang Bot" ) ;
2023-07-04 09:58:35 +01:00
/* Set fakelag to none */
connInfo . setFakeLag ( 0 ) ;
/* Create a new IRC bot instance */
2022-11-06 10:09:15 +00:00
ircBot = new IRCBot ( connInfo ) ;
2022-11-21 09:24:24 +00:00
/* Connect to the server */
2022-11-06 10:09:15 +00:00
ircBot . connect ( ) ;
2022-11-21 09:24:24 +00:00
/* Choose a nickname */
2022-11-06 10:09:15 +00:00
Thread . sleep ( dur ! ( "seconds" ) ( 2 ) ) ;
2022-11-13 18:31:50 +00:00
ircBot . command ( new Message ( "" , "NICK" , nickname ) ) ;
2022-11-06 10:09:15 +00:00
2022-11-21 09:24:24 +00:00
/* Identify oneself */
2022-11-06 10:09:15 +00:00
Thread . sleep ( dur ! ( "seconds" ) ( 2 ) ) ;
2022-11-21 09:24:24 +00:00
//TODO: Clean this string up
2022-11-06 10:09:15 +00:00
ircBot . command ( new Message ( "" , "USER" , "giteabotweb giteabotweb irc.frdeenode.net :Tristan B. Kildaire" ) ) ;
2022-11-21 09:24:24 +00:00
2023-07-04 11:13:31 +01:00
/* Join the requested channels */
2022-11-06 10:09:15 +00:00
Thread . sleep ( dur ! ( "seconds" ) ( 4 ) ) ;
2023-07-04 11:15:59 +01:00
ircBot . joinChannel ( channels ) ;
2022-11-06 10:09:15 +00:00
2022-11-21 09:22:23 +00:00
/* Setup the web server */
2022-11-06 10:09:15 +00:00
HTTPServerSettings httpServerSettings = new HTTPServerSettings ( ) ;
2022-11-21 09:22:23 +00:00
httpServerSettings . port = listenPort ;
httpServerSettings . bindAddresses = listenAddresses ;
2022-11-06 10:09:15 +00:00
2022-11-08 08:19:25 +00:00
/* Create a router and add the supported routes */
2022-11-06 10:09:15 +00:00
URLRouter router = new URLRouter ( ) ;
router . post ( "/commit" , & commitHandler ) ;
router . post ( "/issue" , & issueHandler ) ;
router . post ( "/pullrequest" , & pullRequestHandler ) ;
2022-11-08 08:19:25 +00:00
/* Attach the router to the HTTP server settings */
2022-11-06 10:09:15 +00:00
listenHTTP ( httpServerSettings , router ) ;
/* Starts the vibe-d event engine web server on the main thread */
runApplication ( ) ;
2023-03-11 13:51:31 +00:00
}