slack.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  1. const
  2. WebSocket = require('ws'),
  3. https = require('https'),
  4. sleep = require("sleep").sleep
  5. ,SlackData = require("./slackData.js").SlackData
  6. ,SlackHistory = require("./slackHistory.js").SlackHistory
  7. ,config = require("../config.js")
  8. ,httpsRequest = require('./httpsRequest.js').httpsRequest
  9. ;
  10. const SLACK_ENDPOINT = "https://slack.com/api/"
  11. ,SLACK_HOSTNAME = "slack.com"
  12. ,SLACK_ENDPOINT_PATH = "/api/"
  13. ,GETAPI = {
  14. rtmStart: "rtm.start"
  15. ,oauth: "oauth.access"
  16. ,identityEmail: "users.identity"
  17. ,channelHistory: "channels.history"
  18. ,directHistory: "im.history"
  19. ,groupHistory: "groups.history"
  20. ,starChannel: "stars.add"
  21. ,unstarChannel: "stars.remove"
  22. ,starMsg: "stars.add"
  23. ,unstarMsg: "stars.remove"
  24. ,postMsg: "chat.postMessage"
  25. ,postMeMsg: "chat.meMessage"
  26. ,editMsg: "chat.update"
  27. ,removeMsg: "chat.delete"
  28. ,postFile: "files.upload"
  29. ,setActive: "users.setActive"
  30. ,setPresence: "users.setPresence"
  31. ,emojiList: "emoji.list"
  32. ,slashList: "commands.list"
  33. ,pinMsg: "pins.add"
  34. ,unpinMsg: "pins.remove"
  35. ,listPinned: "pins.list"
  36. ,listMpims: "mpim.list"
  37. ,slashExec: "chat.command"
  38. ,addReaction: "reactions.add"
  39. ,channelList: "users.counts"
  40. ,removeReaction: "reactions.remove"
  41. ,sendAction: "chat.attachmentAction"
  42. ,read: {
  43. group: "groups.mark"
  44. ,im: "im.mark"
  45. ,group: "mpim.mark"
  46. ,channel: "channels.mark"
  47. }
  48. }
  49. ,HISTORY_LENGTH = 35
  50. ,HISTORY_MAX_AGE = 10000// * 60 * 1000
  51. ,UPDATE_LIVE = [
  52. "message"
  53. ,"pin_added"
  54. ,"pin_removed"
  55. ,"reaction_added"
  56. ,"reaction_removed"
  57. ,"star_added"
  58. ,"star_removed"
  59. ] // Message type that affect live history
  60. ;
  61. /**
  62. * @implements {ChatSystem}
  63. **/
  64. function Slack(slackToken, manager) {
  65. this.token = slackToken;
  66. this.manager = manager;
  67. this.rtm = null;
  68. this.rtmId = 1;
  69. this.data = new SlackData(this);
  70. this.history = {};
  71. this.pendingRtm = {};
  72. this.pendingMessages = [];
  73. this.pendingPing = false;
  74. this.connected = false;
  75. this.closing = false;
  76. /** @type {array<String>} */
  77. this.queuedMessages = [];
  78. }
  79. Slack.prototype.getId = function() {
  80. return this.data.team ? this.data.team.id : null;
  81. };
  82. Slack.prototype.onRequest = function() {
  83. if (this.connected === false) {
  84. this.connect();
  85. }
  86. };
  87. Slack.prototype.sendUnsafe = function(msg) {
  88. this.rtm.send(msg);
  89. };
  90. Slack.prototype.send = function(msg, priority) {
  91. try {
  92. this.sendUnsafe(msg);
  93. } catch (e) {
  94. this.connected = false;
  95. console.error("[SLACK] send failed");
  96. if (priority)
  97. this.queuedMessages.push(msg);
  98. this.connect();
  99. return false;
  100. }
  101. return true;
  102. };
  103. Slack.prototype.connect = function(cb) {
  104. var _this = this;
  105. this.connected = undefined;
  106. httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
  107. if (!body || !body.ok) {
  108. _this.error = "Slack API error";
  109. _this.connected = false;
  110. _this.pendingPing = false;
  111. console.error("Slack api responded " +status +" with body " +JSON.stringify(body));
  112. cb && cb(_this);
  113. } else if (status !== 200) {
  114. _this.error = body.error;
  115. _this.connected = false;
  116. _this.pendingPing = false;
  117. console.error("Slack api responded " +status);
  118. cb && cb(_this);
  119. } else {
  120. httpsRequest(SLACK_ENDPOINT +GETAPI.channelList +"?mpim_aware=true" /*+"&include_threads=true"*/ +"&token=" +this.token, (status, channels) => {
  121. if (!channels || !channels.ok) {
  122. _this.error = "Slack API error";
  123. _this.connected = false;
  124. _this.pendingPing = false;
  125. console.error("Slack api responded " +status +" with body " +JSON.stringify(body));
  126. cb && cb(_this);
  127. } else if (status !== 200) {
  128. _this.error = body.error;
  129. _this.connected = false;
  130. _this.pendingPing = false;
  131. console.error("Slack api responded " +status);
  132. cb && cb(_this);
  133. } else {
  134. this.data = new SlackData(this);
  135. // Merge body.channels (missing unread stuff) and channels data (missing members)
  136. body["channels"].forEach(function(channelData) {
  137. for (var i =0, nbChans = channels["channels"].length; i < nbChans; i++) {
  138. if (channels["channels"][i]["id"] === channelData["id"]) {
  139. channelData["unread_count"] = channels["channels"][i]["unread_count"];
  140. break;
  141. }
  142. }
  143. });
  144. body["groups"].forEach(function(channelData) {
  145. for (var i =0, nbChans = channels["groups"].length; i < nbChans; i++) {
  146. if (channels["groups"][i]["id"] === channelData["id"]) {
  147. channelData["unread_count"] = channels["groups"][i]["unread_count"];
  148. break;
  149. }
  150. }
  151. });
  152. // TODO deal with channels["threads"]
  153. _this.data.updateStatic({
  154. team: body["team"],
  155. users: body["users"],
  156. bots: body["bots"],
  157. self: body["self"],
  158. channels: body["channels"],
  159. groups: body["groups"],
  160. ims: channels["ims"],
  161. mpims: channels["mpims"]
  162. }, Date.now());
  163. _this.connectRtm(body.url);
  164. }
  165. });
  166. }
  167. });
  168. };
  169. Slack.prototype.sendCommand = function(room, cmd, arg) {
  170. httpsRequest(
  171. SLACK_ENDPOINT
  172. +GETAPI.slashExec
  173. +"?token=" +this.token
  174. +"&command=" +encodeURIComponent(cmd.name)
  175. +"&disp=" +encodeURIComponent(cmd.name)
  176. +"&channel=" +room.remoteId
  177. +"&text=" +arg);
  178. }
  179. Slack.prototype.sendTyping = function(room) {
  180. this.send('{"id":' +this.rtmId++ +',"type":"typing","channel":"' +room.remoteId +'"}');
  181. }
  182. Slack.prototype.getSlashCommands = function(cb) {
  183. httpsRequest(SLACK_ENDPOINT +GETAPI.slashList +"?token=" +this.token, (status, body) => {
  184. if (!status || !body || !body.ok)
  185. cb(null);
  186. else
  187. cb(body.commands || {});
  188. });
  189. };
  190. Slack.prototype.getEmojis = function(cb) {
  191. httpsRequest(SLACK_ENDPOINT +GETAPI.emojiList +"?token=" +this.token, (status, body) => {
  192. if (!status || !body || !body.ok)
  193. cb(null);
  194. else
  195. cb(body.emoji || {});
  196. });
  197. };
  198. Slack.prototype.poll = function(knownVersion, now, withTyping) {
  199. if (this.connected) {
  200. var updatedCtx = this.data.getUpdates(knownVersion)
  201. ,updatedTyping = withTyping ? this.data.getWhoIsTyping(knownVersion, now) : undefined
  202. ,updatedLive = this.getLiveUpdates(knownVersion);
  203. if (updatedCtx || updatedLive || updatedTyping) {
  204. return {
  205. "static": updatedCtx,
  206. "live": updatedLive,
  207. "typing": updatedTyping,
  208. "v": Math.max(this.data.liveV, this.data.staticV, this.data.typingVersion)
  209. };
  210. }
  211. }
  212. };
  213. /** @return {Object|undefined} */
  214. Slack.prototype.getLiveUpdates = function(knownVersion) {
  215. var result = {};
  216. for (var roomId in this.history) {
  217. var history = this.history[roomId];
  218. if (history.isNew) {
  219. result[roomId] = history.toStatic(0);
  220. history.isNew = false;
  221. }
  222. else {
  223. var roomData = history.toStatic(knownVersion);
  224. if (roomData.length)
  225. result[roomId] = roomData;
  226. }
  227. }
  228. for (var roomId in result) {
  229. return result;
  230. }
  231. return undefined;
  232. };
  233. Slack.prototype.unstackPendingMessages = function() {
  234. for (var i = this.pendingMessages.length -1; i >= 0; i--) {
  235. this.onMessage(this.pendingMessages[0], Date.now());
  236. this.pendingMessages.shift();
  237. }
  238. while (this.queuedMessages.length) {
  239. if (this.send(this.queuedMessages[0])) {
  240. this.queuedMessages.shift();
  241. } else {
  242. break;
  243. }
  244. }
  245. };
  246. Slack.prototype.resetVersions = function(v) {
  247. this.data.team.version = v;
  248. for (var i in this.data.channels)
  249. this.data.channels[i].version = v;
  250. for (var i in this.data.users)
  251. this.data.users[i].version = v;
  252. if (this.data.self && this.data.self.prefs)
  253. this.data.self.prefs.version = v;
  254. this.data.staticV = v;
  255. };
  256. Slack.prototype.onMessage = function(msg, t) {
  257. if (msg["reply_to"] && this.pendingRtm[msg["reply_to"]]) {
  258. var ts = msg["ts"]
  259. ,rtmId = msg["reply_to"];
  260. msg = this.pendingRtm[rtmId];
  261. msg["ts"] = ts;
  262. delete this.pendingRtm[rtmId];
  263. }
  264. if (msg["type"] === "hello" && msg["start"] && msg["start"]["rtm_start"]) {
  265. var _this = this;
  266. _this.getRawMpims((mpims) => {
  267. _this.getEmojis((emojis) => {
  268. _this.getSlashCommands((commands) => {
  269. var msgContent = msg.start.rtm_start;
  270. var now = Date.now();
  271. msgContent.self = msg.self;
  272. msgContent.emojis = emojis;
  273. msgContent.commands = commands;
  274. msgContent.mpims = mpims;
  275. _this.resetVersions(now);
  276. _this.data.updateStatic(msgContent, now);
  277. _this.connected = true;
  278. _this.unstackPendingMessages();
  279. _this.ping();
  280. });
  281. });
  282. });
  283. } else if (this.connected) {
  284. this.data.onMessage(msg, t);
  285. if ((msg["channel"] || msg["channel_id"] || (msg["item"] && msg["item"]["channel"])) && msg["type"] && UPDATE_LIVE.indexOf(msg["type"]) !== -1) {
  286. var channelId = this.data.team.id +'|' +(msg["channel"] || msg["channel_id"] || msg["item"]["channel"])
  287. ,channel = this.data.channels[channelId]
  288. ,histo = this.lazyHistory(channel);
  289. var lastMsg = histo.push(msg, t);
  290. if (lastMsg) {
  291. this.data.liveV = t;
  292. // FIXME not true (edit, etc..)
  293. var messageObject = histo.lastMessage();
  294. if (messageObject && messageObject.userId)
  295. this.data.stopTyping(channelId, messageObject.userId, t);
  296. }
  297. histo.resort();
  298. if (channel)
  299. channel.setLastMsg(lastMsg, t);
  300. }
  301. } else {
  302. this.pendingMessages.push(msg);
  303. }
  304. };
  305. Slack.prototype.getRawMpims = function(cb) {
  306. httpsRequest(SLACK_ENDPOINT +GETAPI.listMpims
  307. +"?token=" +this.token, (status, content) => {
  308. if (status === 200 && content.ok)
  309. cb(content.groups);
  310. else
  311. cb(undefined);
  312. });
  313. };
  314. /**
  315. * @param {SlackChan|SlackGroup|SlackIms} chan
  316. * @param {string} id
  317. * @param {number} ts
  318. **/
  319. Slack.prototype.markRead = function(chan, id, ts) {
  320. var apiURI;
  321. if (chan.remoteId[0] === 'C')
  322. apiURI = SLACK_ENDPOINT+GETAPI.read.channel;
  323. else if (chan.remoteId[0] === 'G')
  324. apiURI = SLACK_ENDPOINT+GETAPI.read.group;
  325. else if (chan.remoteId[0] === 'D')
  326. apiURI = SLACK_ENDPOINT+GETAPI.read.im;
  327. httpsRequest(apiURI
  328. +"?token=" +this.token
  329. +"&channel="+chan.remoteId
  330. +"&ts="+id);
  331. };
  332. Slack.prototype.connectRtm = function(url, cb) {
  333. var _this = this;
  334. this.rtmId = 1;
  335. var protocol = url.substr(0, url.indexOf('://') +3);
  336. url = url.substr(protocol.length);
  337. url = protocol +url.substr(0, url.indexOf('/'))+
  338. "/?flannel=1&token=" +this.token+
  339. "&start_args=" +encodeURIComponent("?simple_latest=true&presence_sub=true&canonical_avatars=true")
  340. this.rtm = new WebSocket(url);
  341. this.rtm.on("message", function(msg) {
  342. if (!_this.connected && cb) {
  343. cb();
  344. }
  345. try {
  346. msg = JSON.parse(msg);
  347. } catch (ex) {
  348. console.error("WTF invalid JSON ", msg);
  349. }
  350. _this.onMessage(msg, Date.now());
  351. });
  352. this.rtm.once("error", function(e) {
  353. _this.connected = false;
  354. _this.pendingPing = false;
  355. console.error(e);
  356. _this.close();
  357. });
  358. this.rtm.once("end", function() {
  359. console.error("RTM hang up");
  360. _this.onClose();
  361. });
  362. };
  363. Slack.prototype.onClose = function() {
  364. this.manager.suicide(this);
  365. };
  366. Slack.prototype.ping = function() {
  367. httpsRequest(SLACK_ENDPOINT+GETAPI.setActive
  368. +"?token=" +this.token);
  369. };
  370. Slack.prototype.rtmPing = function() {
  371. if (this.connected) {
  372. if (this.pendingPing && this.pendingRtm[this.pendingPing]) {
  373. // We could use reconect_url, but we could have missed something important,
  374. // so we restart all over again
  375. this.connected = false;
  376. this.pendingPing = false;
  377. console.error("[SLACK] Ping timeout");
  378. this.connect();
  379. } else {
  380. var rtmId = this.rtmId++;
  381. try {
  382. this.sendUnsafe('{"id":' +rtmId +',"type":"ping"}');
  383. this.pendingRtm[rtmId] = { type: 'ping' };
  384. this.pendingPing = rtmId;
  385. } catch (e) {
  386. this.connected = false;
  387. this.pendingPing = false;
  388. console.error("[SLACK] Ping timeout");
  389. this.connect();
  390. }
  391. }
  392. }
  393. };
  394. Slack.prototype.close = function() {
  395. if (!this.closing) {
  396. this.closing = true;
  397. if (this.rtm)
  398. this.rtm.close();
  399. this.onClose();
  400. }
  401. };
  402. Slack.getUserId = function(code, redirectUri, cb) {
  403. Slack.getOauthToken(code, redirectUri, (teamName, userId, token) => {
  404. httpsRequest(SLACK_ENDPOINT+GETAPI.identityEmail +"?token="+token,
  405. (status, resp) => {
  406. if (status === 200 && resp.ok && resp.user && resp.user.email) {
  407. cb(resp.user.id +'_' +resp.team.id);
  408. } else {
  409. cb(null);
  410. }
  411. });
  412. });
  413. };
  414. Slack.getOauthToken = function(code, redirectUri, cb) {
  415. httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
  416. +"?client_id=" +config.services.Slack.clientId
  417. +"&client_secret=" +config.services.Slack.clientSecret
  418. +"&redirect_uri=" +encodeURIComponent(redirectUri)
  419. +"&code=" +code,
  420. (status, resp) => {
  421. if (status === 200 && resp.ok) {
  422. cb(resp["team_name"], resp["team_id"] +resp["user_id"], resp["access_token"]);
  423. } else {
  424. cb(null);
  425. }
  426. });
  427. };
  428. /**
  429. * @param {SlackChan|SlackGroup|SlackIms} channel
  430. * @param {string} contentType
  431. * @param {function(string|null)} callback
  432. **/
  433. Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
  434. var req = https.request({
  435. hostname: SLACK_HOSTNAME
  436. ,method: 'POST'
  437. ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
  438. +"?token=" +this.token
  439. +"&channels=" +channel.remoteId
  440. ,headers: {
  441. "Content-Type": contentType
  442. }
  443. }, (res) => {
  444. var errorJson;
  445. res.on("data", (chunk) => {
  446. errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
  447. });
  448. res.once("end", () => {
  449. if (res.statusCode === 200) {
  450. callback(null);
  451. } else {
  452. try {
  453. errorJson = JSON.parse(errorJson.toString());
  454. } catch(e) {
  455. callback("error");
  456. return;
  457. }
  458. callback(errorJson["error"] || "error");
  459. }
  460. });
  461. });
  462. return req;
  463. };
  464. function findBoundary() {
  465. const prefix = '-'.repeat(15)
  466. ,alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  467. ,doYouKnowWhoManyTheyAre = alphabet.length // 26 letters in da alphabet
  468. ,nbArg = arguments.length;
  469. for (let i =0; i < doYouKnowWhoManyTheyAre; i++)
  470. bLoop: for (let j =0; j < doYouKnowWhoManyTheyAre; j++) {
  471. const boundary = prefix +alphabet[i] +alphabet[j];
  472. for (let argIndex =0; argIndex < nbArg; argIndex++) {
  473. if (arguments[argIndex].indexOf(boundary) >= 0)
  474. continue bLoop;
  475. }
  476. return boundary;
  477. }
  478. }
  479. function encodeWithBoundary(boundary, data) {
  480. var resp = "";
  481. for (var k in data) {
  482. resp += '--' +boundary +'\r\n';
  483. resp += 'Content-Disposition: form-data; name="' +k +'"\r\n\r\n'
  484. +data[k]
  485. +'\r\n';
  486. };
  487. return resp +'--' +boundary +'--\r\n';
  488. }
  489. /**
  490. * @param {string} serviceId
  491. * @param {Object} payload
  492. * @param {function(string|null)=} callback
  493. **/
  494. Slack.prototype.sendAction = function(serviceId, payload, callback) {
  495. var channel = this.data.channels[payload["channel_id"]]
  496. ,service = this.data.users[serviceId];
  497. if (channel && service) {
  498. payload["channel_id"] = channel.remoteId;
  499. var payloadString = JSON.stringify(payload)
  500. ,boundary = findBoundary(service.remoteId, payloadString)
  501. ,body = encodeWithBoundary(boundary, {
  502. "service_id": service.remoteId
  503. ,"payload": payloadString
  504. });
  505. var req = https.request({
  506. hostname: SLACK_HOSTNAME
  507. ,method: 'POST'
  508. ,path: SLACK_ENDPOINT_PATH +GETAPI.sendAction +"?token=" +this.token
  509. ,headers: {
  510. "Content-Type": "multipart/form-data; boundary=" +boundary,
  511. "Content-Length": body.length
  512. }
  513. }, (res) => {
  514. if (callback) {
  515. var resp = [];
  516. res.on("data", (chunk) => { resp.push(chunk); });
  517. res.once("end", () => {
  518. resp = Buffer.concat(resp).toString();
  519. try {
  520. resp = JSON.parse(resp);
  521. } catch (e) {
  522. resp = null;
  523. }
  524. callback(resp && resp.ok ? resp : false);
  525. });
  526. }
  527. });
  528. req.end(body);
  529. return true;
  530. }
  531. return false;
  532. };
  533. /**
  534. * @param {SlackChan|SlackGroup|SlackIms} channel
  535. * @param {string} contentType
  536. * @param {function(string|null)} callback
  537. **/
  538. Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
  539. var req = https.request({
  540. hostname: SLACK_HOSTNAME
  541. ,method: 'POST'
  542. ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
  543. +"?token=" +this.token
  544. +"&channels=" +channel.remoteId
  545. ,headers: {
  546. "Content-Type": contentType
  547. }
  548. }, (res) => {
  549. var errorJson;
  550. res.on("data", (chunk) => {
  551. errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
  552. });
  553. res.once("end", () => {
  554. if (res.statusCode === 200) {
  555. callback(null);
  556. } else {
  557. try {
  558. errorJson = JSON.parse(errorJson.toString());
  559. } catch(e) {
  560. callback("error");
  561. return;
  562. }
  563. callback(errorJson["error"] || "error");
  564. }
  565. });
  566. });
  567. return req;
  568. };
  569. /**
  570. * @param {SlackChan|SlackGroup|SlackIms} channel
  571. * @param {string} msgId
  572. * @param {string} reaction
  573. **/
  574. Slack.prototype.addReaction = function(channel, msgId, reaction) {
  575. httpsRequest(SLACK_ENDPOINT +GETAPI.addReaction
  576. +"?token=" +this.token
  577. +"&name=" +reaction
  578. +"&channel="+channel.remoteId
  579. +"&timestamp="+msgId);
  580. }
  581. /**
  582. * @param {SlackChan|SlackGroup|SlackIms} channel
  583. * @param {string} msgId
  584. * @param {string} reaction
  585. **/
  586. Slack.prototype.removeReaction = function(channel, msgId, reaction) {
  587. httpsRequest(SLACK_ENDPOINT +GETAPI.removeReaction
  588. +"?token=" +this.token
  589. +"&name=" +reaction
  590. +"&channel="+channel.remoteId
  591. +"&timestamp="+msgId);
  592. }
  593. /**
  594. * @param {SlackChan|SlackGroup|SlackIms} channel
  595. * @param {Array.<string>} text
  596. * @return {string} sent msg
  597. **/
  598. Slack.prototype.sendMeMsg = function(channel, text) {
  599. var text = idify(this.data, text.join("\n"));
  600. httpsRequest(SLACK_ENDPOINT +GETAPI.postMeMsg
  601. +"?token=" +this.token
  602. +"&channel=" +channel.remoteId
  603. +"&text=" +text
  604. +"&as_user=true");
  605. return text;
  606. };
  607. /**
  608. * @param {SlackChan|SlackGroup|SlackIms} channel
  609. **/
  610. Slack.prototype.starChannel = function(channel) {
  611. httpsRequest(SLACK_ENDPOINT +GETAPI.starChannel
  612. +"?token=" +this.token
  613. +"&channel=" +channel.remoteId);
  614. };
  615. /**
  616. * @param {SlackChan|SlackGroup|SlackIms} channel
  617. **/
  618. Slack.prototype.unstarChannel = function(channel) {
  619. httpsRequest(SLACK_ENDPOINT +GETAPI.unstarChannel
  620. +"?token=" +this.token
  621. +"&channel=" +channel.remoteId);
  622. };
  623. /**
  624. * @param {SlackChan|SlackGroup|SlackIms} channel
  625. * @param {string} msgId
  626. **/
  627. Slack.prototype.starMsg = function(channel, msgId) {
  628. httpsRequest(SLACK_ENDPOINT +GETAPI.starChannel
  629. +"?token=" +this.token
  630. +"&channel=" +channel.remoteId
  631. +"&timestamp=" +msgId);
  632. };
  633. /**
  634. * @param {SlackChan|SlackGroup|SlackIms} channel
  635. * @param {string} msgId
  636. **/
  637. Slack.prototype.unstarMsg = function(channel, msgId) {
  638. httpsRequest(SLACK_ENDPOINT +GETAPI.unstarChannel
  639. +"?token=" +this.token
  640. +"&channel=" +channel.remoteId
  641. +"&timestamp=" +msgId);
  642. };
  643. /**
  644. * @param {SlackChan|SlackGroup|SlackIms} channel
  645. * @param {string} msgId
  646. **/
  647. Slack.prototype.pinMsg = function(channel, msgId) {
  648. httpsRequest(SLACK_ENDPOINT +GETAPI.pinMsg
  649. +"?token=" +this.token
  650. +"&channel=" +channel.remoteId
  651. +"&timestamp=" +msgId);
  652. };
  653. /**
  654. * @param {SlackChan|SlackGroup|SlackIms} channel
  655. * @param {string} msgId
  656. **/
  657. Slack.prototype.unpinMsg = function(channel, msgId) {
  658. httpsRequest(SLACK_ENDPOINT +GETAPI.unpinMsg
  659. +"?token=" +this.token
  660. +"&channel=" +channel.remoteId
  661. +"&timestamp=" +msgId);
  662. };
  663. function idify(data, str) {
  664. return str.replace(/(^|\s)@(\S+)/g, function(match, p1, userName) {
  665. for (var i in data.users) {
  666. if (data.users[i].getName() === userName) {
  667. return p1 +"<@" +data.users[i].remoteId +'|' +userName +'>';
  668. }
  669. }
  670. return match;
  671. }).replace(/(^|\s)#(\S+)/g, function(match, p1, chanName) {
  672. for (var i in data.channels) {
  673. if (data.channels[i].name === chanName) {
  674. return p1 +"<#" +data.channels[i].remoteId +'|' +chanName +'>';
  675. }
  676. }
  677. return match;
  678. });
  679. }
  680. /**
  681. * @param {SlackChan|SlackGroup|SlackIms} channel
  682. * @param {Array.<string>} text
  683. * @param {Array.<Object>=} attachments
  684. * @return {string} sent message
  685. **/
  686. Slack.prototype.sendMsg = function(channel, text, attachments) {
  687. if (attachments) {
  688. attachments.forEach((attachmentObj) => {
  689. if (attachmentObj["ts"])
  690. attachmentObj["ts"] /= 1000;
  691. });
  692. var decodedText = idify(this.data, text.join("\n"));
  693. httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
  694. +"?token=" +this.token
  695. +"&channel=" +channel.remoteId
  696. +"&text=" +decodedText
  697. +"&link_names=true"
  698. + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "")
  699. +"&as_user=true");
  700. return fullDecodedText;
  701. } else {
  702. var decodedText = [];
  703. text.forEach(function(i) {
  704. decodedText.push(decodeURIComponent(i));
  705. });
  706. var fullDecodedText = idify(this.data, decodedText.join("\n"));
  707. this.pendingRtm[this.rtmId] = {
  708. type: 'message',
  709. channel: channel.remoteId,
  710. user: this.data.self.remoteId,
  711. text: fullDecodedText
  712. };
  713. this.send('{"id":' +this.rtmId++ +',"type":"message","channel":"' +channel.remoteId +'", "text":' +JSON.stringify(fullDecodedText) +'}', true);
  714. return fullDecodedText;
  715. }
  716. };
  717. /**
  718. * @param {SlackChan|SlackGroup|SlackIms} channel
  719. * @param {string} msgId
  720. **/
  721. Slack.prototype.removeMsg = function(channel, msgId) {
  722. httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg
  723. +"?token=" +this.token
  724. +"&channel=" +channel.remoteId
  725. +"&ts=" +msgId
  726. +"&as_user=true");
  727. };
  728. /**
  729. * @param {SlackChan|SlackGroup|SlackIms} channel
  730. * @param {string} msgId
  731. * @param {string} text
  732. **/
  733. Slack.prototype.editMsg = function(channel, msgId, text) {
  734. httpsRequest(SLACK_ENDPOINT +GETAPI.editMsg
  735. +"?token=" +this.token
  736. +"&channel=" +channel.remoteId
  737. +"&ts=" +msgId
  738. +"&text=" +text.join("\n")
  739. +"&as_user=true");
  740. };
  741. /**
  742. * @param {SlackChan|SlackGroup|SlackIms} target
  743. **/
  744. Slack.prototype.lazyHistory = function(target) {
  745. var histo = this.history[target.id];
  746. if (!histo) {
  747. histo = this.history[target.id] = new SlackHistory(this, target.remoteId, target.id, this.data.team.id +'|', HISTORY_LENGTH, HISTORY_MAX_AGE);
  748. histo.isNew = true;
  749. }
  750. return histo;
  751. };
  752. /**
  753. * @param {SlackChan|SlackGroup|SlackIms} target
  754. **/
  755. Slack.prototype.fetchPinned = function(target) {
  756. var _this = this;
  757. httpsRequest(SLACK_ENDPOINT +GETAPI.listPinned
  758. +"?token="+this.token
  759. +"&channel=" +target.remoteId,
  760. (status, resp) => {
  761. if (status === 200 && resp && resp.ok) {
  762. var msgs = [],
  763. histo = _this.lazyHistory(target);
  764. now = Date.now();
  765. resp.items.forEach(function(msg) {
  766. if (msg.message)
  767. msgs.push(histo.messageFactory(msg.message, now));
  768. // Else file
  769. });
  770. target.pins = msgs;
  771. target.version = Math.max(target.version, now);
  772. _this.data.staticV = Math.max(_this.data.staticV, now);
  773. }
  774. });
  775. };
  776. /**
  777. * @param {SlackChan|SlackGroup|SlackIms} target
  778. **/
  779. Slack.prototype.fetchHistory = function(target, cb, count, firstMsgId) {
  780. var _this = this
  781. ,baseUrl = ""
  782. ,targetId = target.remoteId;
  783. if (targetId[0] === 'D') {
  784. baseUrl = SLACK_ENDPOINT +GETAPI.directHistory;
  785. } else if (targetId[0] === 'C') {
  786. baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory;
  787. } else if (targetId[0] === 'G') {
  788. baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory;
  789. }
  790. httpsRequest(baseUrl
  791. +"?token="+this.token
  792. +"&channel=" +targetId
  793. +"&include_pin_count=true"
  794. +(firstMsgId ? ("&inclusive=true&latest=" +firstMsgId) : "")
  795. +"&count=" +(count || 100),
  796. (status, resp) => {
  797. var history = [];
  798. if (status === 200 && resp && resp.ok) {
  799. var histo = this.lazyHistory(target);
  800. resp.messages.forEach((respMsg) => {
  801. respMsg["id"] = respMsg["ts"];
  802. history.push(histo.messageFactory(histo.prepareMessage(respMsg)));
  803. });
  804. if (!target.pins || target.pins.length !== resp["pin_count"]) {
  805. if (!resp["pin_count"]) {
  806. target.pins = [];
  807. target.version = Math.max(target.version, Date.now());
  808. _this.data.staticV = Math.max(_this.data.staticV, target.version);
  809. } else {
  810. _this.fetchPinned(target);
  811. }
  812. }
  813. }
  814. cb(history);
  815. });
  816. };
  817. Slack.prototype.getChatContext = function() {
  818. return this.data;
  819. };
  820. module.exports.Slack = Slack;