1 /// Main module for slack-d 2 module slack; 3 4 import std.net.curl, std.conv; 5 6 public import std.datetime.systime; 7 public import std.json; 8 9 /// Base Slack Web API URL. 10 package immutable slackApiUrl = "https://slack.com/api/"; 11 12 /** 13 * Contains the response from a Slack REST API call encoded as JSON. 14 * See_Also: $(D std.json) 15 */ 16 struct Response { 17 /// Returns `true` if the last API call succeeded, `false` otherwise. 18 auto opCast(T : bool)() const { return _value["ok"].boolean; } 19 /// Returns the JSON response as a string. 20 auto toString() const { return toJSON(_value); } 21 /// Assign another Response to this one. 22 auto ref opAssign(inout Response rhs) { _value = rhs._value; return this; } 23 /// Returns the value for the given key from the JSON response. 24 auto opIndex(string key) const { return _value[key]; } 25 package: 26 /// Response may only be constructed in this package. 27 this(const char[] response) { _value = parseJSON(response); } 28 @disable this(); 29 private: 30 JSONValue _value; 31 } 32 33 unittest { 34 import std.exception, core.exception; 35 auto r = Response(""); 36 assertThrown!JSONException(to!bool(r)); 37 assert(r.toString() == "null"); 38 39 r = Response(`{"ok":false}`); 40 assert(r.toString() == `{"ok":false}`); 41 assert(r["ok"].boolean == false); 42 assert(to!bool(r) == false); 43 44 r = Response(`{"ok":true}`); 45 assert(r.toString() == `{"ok":true}`); 46 assert(r["ok"].boolean == true); 47 assert(to!bool(r) == true); 48 49 auto r2 = Response(`{"ok":true}`); 50 assert(r == r2); 51 52 r = r2; 53 assert(r == r2); 54 assert(r2["ok"].boolean == true); 55 } 56 57 /** 58 * Holds the authorization token and the current channel for further REST API calls. 59 */ 60 struct Slack { 61 /** 62 * Constructs a new Slack object and stores the token and channel for later use. 63 * Params: 64 * token = Slack BOT token to use for REST API authorization. 65 * channel = Channel to use. Default is `general`. 66 */ 67 this(string token, string channel = "general") { 68 _token = token; 69 _channel = channel; 70 } // ctor() 71 72 /** 73 * Sends a message to a channel. 74 * Params: 75 * msgString = Plain message string 76 * Returns: JSON response from REST API endpoint. 77 * See_Also: $(LINK https://api.slack.com/methods/chat.postMessage) 78 */ 79 Response postMessage(string msgString) const { 80 string[string] data = ["token": _token, "channel": _channel, "text": msgString]; 81 auto http = HTTP(); 82 http.addRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 83 return Response(post(slackApiUrl ~ "chat.postMessage", data, http)); 84 } // postMessage() 85 86 /** 87 * Sends a message to a channel. 88 * Params: 89 * jsonMsg = Message containing either "attachments" or "blocks" for more elaborate formatting 90 * Returns: JSON response from REST API endpoint. 91 * See_Also: $(LINK https://api.slack.com/methods/chat.postMessage) 92 */ 93 Response postMessage(JSONValue jsonMsg) const { 94 string[string] data = ["token": _token, "channel": _channel]; 95 if ("attachments" in jsonMsg) 96 data["attachments"] = toJSON(jsonMsg["attachments"]); 97 else if ("blocks" in jsonMsg) 98 data["blocks"] = toJSON(jsonMsg["blocks"]); 99 else 100 assert(0, `missing "attachment" or "blocks"`); 101 auto http = HTTP(); 102 http.addRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 103 return Response(post(slackApiUrl ~ "chat.postMessage", data, http)); 104 } // postMessage() 105 106 /** 107 * Fetches a conversation's history of messages and events. 108 * Params: 109 * channelId = Conversation ID to fetch history for. 110 * oldest = SysTime of oldest entry to look for (default=0) 111 * latest = SysTime of latest entry to look for (default=now) 112 * Returns: JSON response from REST API endpoint. 113 * See_Also: $(LINK https://api.slack.com/methods/conversations.history) 114 */ 115 Response conversationsHistory(string channelId, SysTime oldest = SysTime(), SysTime latest = SysTime()) const { 116 string[string] data = ["token": _token, "channel": channelId]; 117 if (oldest != SysTime.init) data["oldest"] = to!string(oldest.toUnixTime()); 118 if (latest != SysTime.init) data["latest"] = to!string(latest.toUnixTime()); 119 auto http = HTTP(); 120 http.addRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 121 return Response(post(slackApiUrl ~ "conversations.history", data, http)); 122 } 123 124 /** 125 * Lists all channels in a Slack team. 126 * Returns: JSON response from REST API endpoint. 127 * See_Also: $(LINK https://api.slack.com/methods/conversations.list) 128 */ 129 Response conversationsList() const { 130 auto http = HTTP(); 131 http.addRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 132 return Response(post(slackApiUrl ~ "conversations.list", 133 ["token": _token, "exclude_archived": "true"], http)); 134 } 135 136 /// Sets the current channel. 137 @property void channel(string channel) { _channel = channel; } 138 /// Returns the current channel. 139 @property string channel() const { return _channel; } 140 141 private: 142 string _channel, _token; 143 } 144 145 unittest { 146 import std.process, std.format, std.system, core.cpuid, std.uuid; 147 148 auto token = environment.get("SLACK_TOKEN"); 149 assert(token !is null, "please define the SLACK_TOKEN environment variable!"); 150 151 auto slack = Slack(token); 152 153 auto msg = format("%s OS: %s (%s) CPU: %s %s with %s cores (%s threads)", 154 randomUUID(), os, endian, vendor, processor, coresPerCPU, threadsPerCPU); 155 auto r = slack.postMessage(msg); 156 assert(to!bool(r), to!string(r)); 157 158 auto attachments = `{"attachments":[{ 159 "fallback": "A message with more elaborate formatting", 160 "pretext": "` ~ msg ~ `", 161 "title": "Don't click here!", 162 "title_link": "https://youtu.be/dQw4w9WgXcQ", 163 "text": "You know the rules and so do I", 164 "color": "#7CD197", 165 "image_url": "https://assets.amuniversal.com/086aac509ee3012f2fe600163e41dd5b" 166 }]}`; 167 r = slack.postMessage(parseJSON(attachments)); 168 assert(to!bool(r), to!string(r)); 169 170 auto blocks = `{"blocks":[ 171 { 172 "type": "header", 173 "text": { 174 "type": "plain_text", 175 "text": "` ~ msg ~ `", 176 "emoji": true 177 } 178 }, 179 { 180 "type": "section", 181 "fields": [ 182 { 183 "type": "mrkdwn", 184 "text": "*Type:*\nPaid Time Off" 185 }, 186 { 187 "type": "mrkdwn", 188 "text": "*Created by:*\n<example.com|Fred Enriquez>" 189 } 190 ] 191 }, 192 { 193 "type": "section", 194 "fields": [ 195 { 196 "type": "mrkdwn", 197 "text": "*When:*\nAug 10 - Aug 13" 198 }, 199 { 200 "type": "mrkdwn", 201 "text": "*Type:*\nPaid time off" 202 } 203 ] 204 }, 205 { 206 "type": "section", 207 "fields": [ 208 { 209 "type": "mrkdwn", 210 "text": "*Hours:*\n16.0 (2 days)" 211 }, 212 { 213 "type": "mrkdwn", 214 "text": "*Remaining balance:*\n32.0 hours (4 days)" 215 } 216 ] 217 }, 218 { 219 "type": "section", 220 "text": { 221 "type": "mrkdwn", 222 "text": "<https://example.com|View request>" 223 } 224 }, 225 { 226 "type": "divider" 227 }, 228 { 229 "type": "section", 230 "text": { 231 "type": "mrkdwn", 232 "text": "Pick a date for the deadline." 233 }, 234 "accessory": { 235 "type": "datepicker", 236 "initial_date": "1990-04-28", 237 "placeholder": { 238 "type": "plain_text", 239 "text": "Select a date", 240 "emoji": true 241 }, 242 "action_id": "datepicker-action" 243 } 244 }, 245 { 246 "type": "section", 247 "text": { 248 "type": "mrkdwn", 249 "text": "Section block with a timepicker" 250 }, 251 "accessory": { 252 "type": "timepicker", 253 "initial_time": "13:37", 254 "placeholder": { 255 "type": "plain_text", 256 "text": "Select time", 257 "emoji": true 258 }, 259 "action_id": "timepicker-action" 260 } 261 }, 262 { 263 "type": "section", 264 "text": { 265 "type": "mrkdwn", 266 "text": "This is a section block with an accessory image." 267 }, 268 "accessory": { 269 "type": "image", 270 "image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg", 271 "alt_text": "cute cat" 272 } 273 }, 274 { 275 "type": "divider" 276 }, 277 { 278 "type": "image", 279 "image_url": "https://i1.wp.com/thetempest.co/wp-content/uploads/2017/08/The-wise-words-of-Michael-Scott-Imgur-2.jpg?w=1024&ssl=1", 280 "alt_text": "inspiration" 281 } 282 ]}`; 283 284 r = slack.postMessage(parseJSON(blocks)); 285 assert(to!bool(r), to!string(r)); 286 287 r = slack.conversationsList(); 288 assert(to!bool(r), to!string(r)); 289 290 import std.range.primitives; 291 292 string channelId; 293 foreach (channel; r["channels"].array) 294 if (channel["name"].str == slack.channel) 295 channelId = channel["id"].str; 296 assert(channelId.length > 0, to!string(r)); 297 298 import core.time : seconds; 299 r = slack.conversationsHistory(channelId, Clock.currTime() - seconds(10)); 300 assert(to!bool(r), to!string(r)); 301 302 auto foundAttachments = false; 303 auto foundBlocks = false; 304 auto foundPlain = false; 305 foreach (message; r["messages"].array) { 306 if ("text" in message && message["text"].str == msg) 307 foundPlain = true; 308 if ("attachments" in message && message["attachments"][0]["pretext"].str == msg) 309 foundAttachments = true; 310 if ("blocks" in message && "text" in message["blocks"][0] && message["blocks"][0]["text"]["text"].str == msg) 311 foundBlocks = true; 312 } 313 assert(foundPlain, "did not find plain message in history"); 314 assert(foundBlocks, "did not find blocks message in history"); 315 assert(foundAttachments, "did not find attachments message in history"); 316 317 slack.channel = "some_channel"; 318 assert(slack.channel == "some_channel"); 319 } 320 321 unittest { 322 import std.process, std.format, std.system, core.cpuid, std.uuid; 323 324 auto token = environment.get("SLACK_TOKEN"); 325 assert(token !is null, "please define the SLACK_TOKEN environment variable!"); 326 327 auto slack = Slack(token); 328 329 auto msg = `this&that \\[@\\] an!// '; < ? >> , <~ ~ @ # \\} \\{ * & % $ ! >~<`; 330 auto r = slack.postMessage(msg); 331 assert(to!bool(r), to!string(r)); 332 333 auto attachments = `{"attachments":[{ 334 "fallback": "` ~ msg ~ `", 335 "pretext": "` ~ msg ~ `", 336 "text": "` ~ msg ~ `", 337 "color": "#7CD197" 338 }]}`; 339 r = slack.postMessage(parseJSON(attachments)); 340 assert(to!bool(r), to!string(r)); 341 }