slack-smart-bot 1.11.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -3
  3. data/lib/slack/smart-bot/comm/respond.rb +8 -2
  4. data/lib/slack/smart-bot/comm/set_status.rb +21 -0
  5. data/lib/slack/smart-bot/comm.rb +1 -0
  6. data/lib/slack/smart-bot/commands/general/add_memo_team.rb +117 -0
  7. data/lib/slack/smart-bot/commands/general/add_vacation.rb +51 -0
  8. data/lib/slack/smart-bot/commands/general/delete_memo_team.rb +69 -0
  9. data/lib/slack/smart-bot/commands/general/delete_team.rb +21 -1
  10. data/lib/slack/smart-bot/commands/general/remove_vacation.rb +27 -0
  11. data/lib/slack/smart-bot/commands/general/see_announcements.rb +1 -1
  12. data/lib/slack/smart-bot/commands/general/see_statuses.rb +1 -1
  13. data/lib/slack/smart-bot/commands/general/see_teams.rb +182 -32
  14. data/lib/slack/smart-bot/commands/general/see_vacations.rb +58 -0
  15. data/lib/slack/smart-bot/commands/general/see_vacations_team.rb +136 -0
  16. data/lib/slack/smart-bot/commands/general/set_memo_status.rb +58 -0
  17. data/lib/slack/smart-bot/commands/general/update_team.rb +22 -1
  18. data/lib/slack/smart-bot/commands/general_bot_commands.rb +174 -7
  19. data/lib/slack/smart-bot/commands/on_bot/general/bot_stats.rb +21 -5
  20. data/lib/slack/smart-bot/commands/on_bot/kill_repl.rb +32 -0
  21. data/lib/slack/smart-bot/commands/on_bot/repl.rb +1 -0
  22. data/lib/slack/smart-bot/commands/on_bot/run_repl.rb +113 -33
  23. data/lib/slack/smart-bot/commands/on_master/admin_master/publish_announcements.rb +3 -1
  24. data/lib/slack/smart-bot/commands.rb +8 -0
  25. data/lib/slack/smart-bot/process.rb +17 -7
  26. data/lib/slack/smart-bot/treat_message.rb +11 -1
  27. data/lib/slack/smart-bot/utils/check_vacations.rb +43 -0
  28. data/lib/slack/smart-bot/utils/get_admins_channels.rb +23 -3
  29. data/lib/slack/smart-bot/utils/get_command_ids.rb +1 -1
  30. data/lib/slack/smart-bot/utils/get_help.rb +4 -3
  31. data/lib/slack/smart-bot/utils/get_vacations.rb +22 -0
  32. data/lib/slack/smart-bot/utils/save_stats.rb +9 -2
  33. data/lib/slack/smart-bot/utils/save_status.rb +1 -1
  34. data/lib/slack/smart-bot/utils/update_admins_channels.rb +20 -3
  35. data/lib/slack/smart-bot/utils/update_vacations.rb +16 -0
  36. data/lib/slack/smart-bot/utils.rb +3 -0
  37. data/lib/slack-smart-bot.rb +22 -2
  38. data/whats_new.txt +12 -17
  39. metadata +19 -7
@@ -10,7 +10,6 @@ def general_bot_commands(user, command, dest, files = [])
10
10
  end
11
11
  display_name = user.profile.display_name
12
12
  end
13
-
14
13
  case command
15
14
 
16
15
 
@@ -76,7 +75,7 @@ def general_bot_commands(user, command, dest, files = [])
76
75
  # help: command_id: :add_announcement
77
76
  # help:
78
77
  when /\A\s*(add|create)\s+(red\s+|green\s+|white\s+|yellow\s+)?(announcement|statement|declaration|message)\s+(.+)\s*\z/i,
79
- /\A\s*(add|create)\s+(:\w+:)\s+(announcement|statement|declaration|message)\s+(.+)\s*\z/i
78
+ /\A\s*(add|create)\s+(:[\w\-]+:)\s+(announcement|statement|declaration|message)\s+(.+)\s*\z/i
80
79
  type = $2.to_s.downcase.strip
81
80
  type = 'white' if type == ''
82
81
  message = $4
@@ -122,10 +121,10 @@ def general_bot_commands(user, command, dest, files = [])
122
121
  # help: <https://github.com/MarioRuiz/slack-smart-bot#announcements|more info>
123
122
  # help: command_id: :see_announcements
124
123
  # help:
125
- when /\A\s*see\s+(red\s+|green\s+|white\s+|yellow\s+|:\w+:\s+)?(announcements|statements|declarations|messages)()\s*\z/i,
124
+ when /\A\s*see\s+(red\s+|green\s+|white\s+|yellow\s+|:[\w\-]+:\s+)?(announcements|statements|declarations|messages)()\s*\z/i,
126
125
  /\A\s*see\s+(all\s+)?(announcements|statements|declarations|messages)()\s*\z/i,
127
- /\A\s*see\s+(red\s+|green\s+|white\s+|yellow\s+|:\w+:\s+)?(announcements|statements|declarations|messages)\s+#([\w\-]+)\s*\z/i,
128
- /\A\s*see\s+(red\s+|green\s+|white\s+|yellow\s+|:\w+:\s+)?(announcements|statements|declarations|messages)\s+<#(\w+)\|.*>\s*\z/i
126
+ /\A\s*see\s+(red\s+|green\s+|white\s+|yellow\s+|:[\w\-]+:\s+)?(announcements|statements|declarations|messages)\s+#([\w\-]+)\s*\z/i,
127
+ /\A\s*see\s+(red\s+|green\s+|white\s+|yellow\s+|:[\w\-]+:\s+)?(announcements|statements|declarations|messages)\s+<#(\w+)\|.*>\s*\z/i
129
128
 
130
129
  type = $1.to_s.downcase.strip
131
130
  channel = $3.to_s
@@ -387,6 +386,94 @@ def general_bot_commands(user, command, dest, files = [])
387
386
  info = Thread.current[:command_orig].to_s.gsub("\u00A0", " ").scan(/^[^:]+:\s*(.+)\s*$/im).join
388
387
  add_team(user, name, options, info)
389
388
 
389
+ # help: ----------------------------------------------
390
+ # help: `add TYPE to TEAM_NAME team : MESSAGE`
391
+ # help: `add private TYPE to TEAM_NAME team : MESSAGE`
392
+ # help: `add personal TYPE to TEAM_NAME team : MESSAGE`
393
+ # help: `add TYPE to TEAM_NAME team TOPIC : MESSAGE`
394
+ # help: `add private TYPE to TEAM_NAME team TOPIC : MESSAGE`
395
+ # help: `add personal TYPE to TEAM_NAME team TOPIC : MESSAGE`
396
+ # help: It will add a memo to the team. The memos will be displayed with the team info.
397
+ # help: Only team members can add a memo.
398
+ # help: TYPE: memo, note, issue, task, feature, bug, jira, github
399
+ # help: TOPIC: one word, a-z, A-Z, 0-9, - and _
400
+ # help: If private then the memo will be only displayed to team members on a DM or the members channel.
401
+ # help: If personal then the memo will be only displayed to the creator on a DM.
402
+ # help: In case jira type supplied:
403
+ # help: The message should be an JQL URL, JQL string or an issue URL.
404
+ # help: To be able to use it you need to specify on the SmartBot settings the credentials.
405
+ # help: In case no TOPIC is supplied then it will create automatically the topics from the labels specified on every JIRA issue
406
+ # help: In case github type supplied:
407
+ # help: The message should be a github URL. You can filter by state (open/closed/all) and labels
408
+ # help: To be able to use it you need to specify on the SmartBot settings the github token.
409
+ # help: In case no TOPIC is supplied then it will create automatically the topics from the labels specified on every Github issue
410
+ # help: Examples:
411
+ # help: _add memo to sales team : Add tests for Michigan feature_
412
+ # help: _add private note to sales team : Bills will need to be deployed before Friday_
413
+ # help: _add memo to dev team web : Check last version_
414
+ # help: _add private bug to dev team SRE : Logs should not be accessible from outside VPN_
415
+ # help: _add memo sales team : Add tests for Michigan feature_
416
+ # help: _add memo team sales: Add tests for Michigan feature_
417
+ # help: _add personal memo team sales: Check my vacations_
418
+ # help: _add jira to sales team : labels = SalesT AND status != Done_
419
+ # help: _add github to sales team : PeterBale/SalesBoom/issues?state=open&labels=bug_
420
+ # help: _add github to sales team dev: PeterBale/DevProject/issues/71_
421
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#teams|more info>
422
+ # help: command_id: :add_memo_team
423
+ # help:
424
+ when /\A\s*add\s+(private\s+|personal\s+)?(memo|note|issue|task|feature|bug|jira|github)\s+(to\s+)?team\s+([\w\-]+)\s*([^:]+)?\s*:\s+(.+)\s*\z/im,
425
+ /\A\s*add\s+(private\s+|personal\s+)?(memo|note|issue|task|feature|bug|jira|github)\s+(to\s+)?([\w\-]+)\s+team\s*([^:]+)?\s*:\s+(.+)\s*\z/im
426
+ privacy = $1.to_s.strip.downcase
427
+ type = $2.downcase
428
+ team_name = $4.downcase
429
+ topic = $5.to_s.strip
430
+ message = Thread.current[:command_orig].to_s.gsub("\u00A0", " ").scan(/^[^:]+:\s*(.+)\s*$/im).join
431
+ add_memo_team(user, privacy, team_name, topic, type, message)
432
+
433
+ # help: ----------------------------------------------
434
+ # help: `delete memo ID from TEAM_NAME team`
435
+ # help: It will delete the supplied memo ID on the team specified.
436
+ # help: aliases for memo: note, issue, task, feature, bug, jira, github
437
+ # help: You have to be a member of the team, the creator or a Master admin to be able to delete a memo.
438
+ # help: Examples:
439
+ # help: _delete memo 32 from sales team_
440
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#teams|more info>
441
+ # help: command_id: :delete_memo_team
442
+ # help:
443
+ when /\A\s*(delete|remove)\s+(memo|note|issue|task|feature|bug|jira|github)\s+(\d+)\s+(from|on)\s+team\s+([\w\-]+)\s*\z/i,
444
+ /\A\s*(delete|remove)\s+(memo|note|issue|task|feature|bug|jira|github)\s+(\d+)\s+(from|on)\s+([\w\-]+)\s+team\s*\z/i
445
+ memo_id = $3
446
+ team_name = $5.downcase
447
+ delete_memo_team(user, team_name, memo_id)
448
+
449
+ # help: ----------------------------------------------
450
+ # help: `set memo ID on TEAM_NAME team STATUS`
451
+ # help: `set STATUS on memo ID TEAM_NAME team`
452
+ # help: It will assign to the ID specified the emoticon status indicated.
453
+ # help: aliases for memo: note, issue, task, feature, bug
454
+ # help: This command will be only for memo, note, issue, task, feature, bug. Not for jira or github.
455
+ # help: You have to be a member of the team, the creator or a Master admin to be able to set a status.
456
+ # help: Examples:
457
+ # help: _set memo 32 on sales team :runner:_
458
+ # help: _set bug 7 on team sales :heavy_check_mark:_
459
+ # help: _set :runner: on memo 6 sales team_
460
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#teams|more info>
461
+ # help: command_id: :set_memo_status
462
+ # help:
463
+ when /\A\s*(set)\s+(memo|note|issue|task|feature|bug)\s+(\d+)\s+on\s+team\s+([\w\-]+)\s+(:[\w\-]+:)\s*\z/i,
464
+ /\A\s*(set)\s+(memo|note|issue|task|feature|bug)\s+(\d+)\s+on\s+([\w\-]+)\s+team\s+(:[\w\-]+:)\s*\z/i
465
+ memo_id = $3
466
+ team_name = $4.downcase
467
+ status = $5
468
+ set_memo_status(user, team_name, memo_id, status)
469
+
470
+ when /\A\s*(set)\s+(:[\w\-]+:)\s+on\s+(memo|note|issue|task|feature|bug)\s+(\d+)\s+team\s+([\w\-]+)\s*\z/i,
471
+ /\A\s*(set)\s+(:[\w\-]+:)\s+on\s+(memo|note|issue|task|feature|bug)\s+(\d+)\s+([\w\-]+)\s+team\s*\z/i
472
+ memo_id = $4
473
+ team_name = $5.downcase
474
+ status = $2
475
+ set_memo_status(user, team_name, memo_id, status)
476
+
390
477
  # help: ----------------------------------------------
391
478
  # help: `see teams`
392
479
  # help: `see team TEAM_NAME`
@@ -469,9 +556,9 @@ def general_bot_commands(user, command, dest, files = [])
469
556
  type = $1.downcase.to_sym
470
557
  name = $2.downcase
471
558
  member_type = $3.downcase
472
- message = Thread.current[:command_orig].to_s.gsub("\u00A0", " ").scan(/\A\s*[^\s]+\s*\w+\s+team\s+[\w\-]+\s+[\w\-]+\s+(.+)\s*\z/im).join
559
+ message = Thread.current[:command_orig].to_s.gsub("\u00A0", " ").scan(/\A\s*[\w]+\s+\w+\s+team\s+[\w\-]+\s+(.+)\s*\z/im).join
473
560
  if message == ''
474
- message = Thread.current[:command_orig].to_s.gsub("\u00A0", " ").scan(/\A\s*[^\s]+\s*\w+\s+[\w\-]+\s+team\s+[\w\-]+\s+(.+)\s*\z/im).join
561
+ message = Thread.current[:command_orig].to_s.gsub("\u00A0", " ").scan(/\A\s*[\w]+\s+team\s+\w+\s+[\w\-]+\s+(.+)\s*\z/im).join
475
562
  end
476
563
  ping_team(user, type, name, member_type, message)
477
564
 
@@ -488,6 +575,86 @@ def general_bot_commands(user, command, dest, files = [])
488
575
  name = $2.downcase
489
576
  delete_team(user, name)
490
577
 
578
+
579
+ # help: ----------------------------------------------
580
+ # help: `add vacation from YYYY/MM/DD to YYYY/MM/DD`
581
+ # help: `add vacation YYYY/MM/DD`
582
+ # help: `add sick from YYYY/MM/DD to YYYY/MM/DD`
583
+ # help: `add sick YYYY/MM/DD`
584
+ # help: `add sick child YYYY/MM/DD`
585
+ # help: It will add the supplied period to your plan.
586
+ # help: Instead of YYYY/MM/DD you can use 'today' or 'tomorrow' or 'next week'
587
+ # help: To see your plan call `see my time off`
588
+ # help: If you want to see the vacation plan for the team `see team NAME`
589
+ # help: Also you can see the vacation plan for the team for a specific period: `vacations team NAME YYYY/MM/DD`
590
+ # help: The SmartBot will automatically set the users status to :palm_tree:, :baby: or :face_with_thermometer: and the expiration date.
591
+ # help: Examples:
592
+ # help: _add vacation from 2022/10/01 to 2022/10/22_
593
+ # help: _add sick 2022/08/22_
594
+ # help: _add vacation tomorrow_
595
+ # help: _add sick baby today_
596
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#time-off-management|more info>
597
+ # help: command_id: :add_vacation
598
+ # help:
599
+ when /\A\s*add\s+(sick|vacation|sick\s+baby|sick\s+child)\s+from\s+(\d\d\d\d\/\d\d\/\d\d)\s+to\s+(\d\d\d\d\/\d\d\/\d\d)\s*\z/i,
600
+ /\A\s*add\s+(sick|vacation|sick\s+baby|sick\s+child)\s+from\s+(\d\d\d\d-\d\d-\d\d)\s+to\s+(\d\d\d\d-\d\d-\d\d)\s*\z/i,
601
+ /\A\s*add\s+(sick|vacation|sick\s+baby|sick\s+child)\s+(\d\d\d\d-\d\d-\d\d)()\s*\z/i,
602
+ /\A\s*add\s+(sick|vacation|sick\s+baby|sick\s+child)\s+(\d\d\d\d\/\d\d\/\d\d)()\s*\z/i,
603
+ /\A\s*add\s+(sick|vacation|sick\s+baby|sick\s+child)\s+(today|tomorrow|next\sweek)()\s*\z/i
604
+ type = $1
605
+ from = $2.downcase
606
+ to = $3
607
+ add_vacation(user, type, from, to)
608
+
609
+ # help: ----------------------------------------------
610
+ # help: `remove vacation ID`
611
+ # help: `remove vacation period ID`
612
+ # help: `remove sick period ID`
613
+ # help: `remove time off period ID`
614
+ # help: `delete vacation ID`
615
+ # help: It will remove the specified period from your vacations/sick periods.
616
+ # help: Examples:
617
+ # help: _remove vacation 20_
618
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#time-off-management|more info>
619
+ # help: command_id: :remove_vacation
620
+ # help:
621
+ when /\A\s*(delete|remove)\s+(vacation|sick|time\s+off)(\s+period)?\s+(\d+)\s*\z/i
622
+ vacation_id = $4
623
+ remove_vacation(user, vacation_id)
624
+
625
+ # help: ----------------------------------------------
626
+ # help: `see my vacations`
627
+ # help: `see my time off`
628
+ # help: `see vacations @USER`
629
+ # help: It will display current and past time off.
630
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#time-off-management|more info>
631
+ # help: command_id: :see_vacations
632
+ # help:
633
+ when /\A\s*see\s+my\s+vacations\s*()\z/i,
634
+ /\A\s*see\s+my\s+time\s+off\s*()\z/i,
635
+ /\A\s*see\s+time\s+off\s+<@(\w+)>\s*\z/i,
636
+ /\A\s*see\s+vacations\s+<@(\w+)>\s*\z/i
637
+ from_user = $1
638
+ see_vacations(user, from_user: from_user)
639
+
640
+ # help: ----------------------------------------------
641
+ # help: `vacations team NAME`
642
+ # help: `time off team NAME`
643
+ # help: `vacations team NAME YYYY/MM/DD`
644
+ # help: `time off team NAME YYYY/MM/DD`
645
+ # help: It will display the time off plan for the team specified.
646
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#time-off-management|more info>
647
+ # help: command_id: :see_vacations_team
648
+ # help:
649
+ when /\A\s*(see\s+)?(vacations|time\s+off)\s+team\s+([\w\-]+)\s*(\d\d\d\d\/\d\d\/\d\d)?\s*\z/i,
650
+ /\A\s*(see\s+)?(vacations|time\s+off)\s+([\w\-]+)\s+team\s*(\d\d\d\d\/\d\d\/\d\d)?\s*\z/i,
651
+ /\A\s*(see\s+)?(vacations|time\s+off)\s+team\s+([\w\-]+)\s*(\d\d\d\d-\d\d-\d\d)?\s*\z/i,
652
+ /\A\s*(see\s+)?(vacations|time\s+off)\s+([\w\-]+)\s+team\s*(\d\d\d\d-\d\d-\d\d)?\s*\z/i
653
+ team_name = $3.downcase
654
+ date = $4.to_s
655
+ date = Date.today.strftime("%Y/%m/%d") if date.empty?
656
+ see_vacations_team(user, team_name, date)
657
+
491
658
  else
492
659
  return false
493
660
  end
@@ -295,11 +295,27 @@ class SlackSmartBot
295
295
  users_attachment = []
296
296
  if user==''
297
297
  users = rows.user_id.uniq.sort
298
- rows.user_id.each do |usr|
299
- user_info = @users.select { |u| u.id == usr or (u.key?(:enterprise_user) and u.enterprise_user.id == usr) }[-1]
300
- unless user_info.nil? or user_info.is_app_user or user_info.is_bot
301
- tzone_users[user_info.tz_label] ||= 0
302
- tzone_users[user_info.tz_label] += 1
298
+ if rows[0].key?(:time_zone) #then save_stats is saving the time zone already
299
+ rows.time_zone.each do |time_zone|
300
+ unless time_zone == ''
301
+ tzone_users[time_zone] ||= 0
302
+ tzone_users[time_zone] += 1
303
+ end
304
+ end
305
+ else
306
+ rows.user_id.each_with_index do |usr, i|
307
+ if rows[i].values.size >= 12 #then save_stats is saving the time zone already but not all the data
308
+ unless rows[i].values[11] == ''
309
+ tzone_users[rows[i].values[11]] ||= 0
310
+ tzone_users[rows[i].values[11]] += 1
311
+ end
312
+ else
313
+ user_info = @users.select { |u| u.id == usr or (u.key?(:enterprise_user) and u.enterprise_user.id == usr) }[-1]
314
+ unless user_info.nil? or user_info.is_app_user or user_info.is_bot
315
+ tzone_users[user_info.tz_label] ||= 0
316
+ tzone_users[user_info.tz_label] += 1
317
+ end
318
+ end
303
319
  end
304
320
  end
305
321
  if users.size > 10
@@ -0,0 +1,32 @@
1
+ class SlackSmartBot
2
+ # help: ----------------------------------------------
3
+ # help: `kill repl RUN_REPL_ID`
4
+ # help: Will kill a running repl previously executed with 'run repl' command.
5
+ # help: Only the user that run the repl or a master admin can kill the repl.
6
+ # help: Example:
7
+ # help: _kill repl X33JK_
8
+ # help: <https://github.com/MarioRuiz/slack-smart-bot#repl|more info>
9
+ # help: command_id: :kill_repl
10
+ # help:
11
+ def kill_repl(dest, user, repl_id)
12
+ #todo: add tests
13
+ if has_access?(__method__, user)
14
+ save_stats(__method__)
15
+ if !@run_repls.key?(repl_id)
16
+ respond "The run repl with id #{repl_id} doesn't exist"
17
+ elsif @run_repls[repl_id].user != user.name and !config.masters.include?(user.name)
18
+ respond "Only #{@run_repls[repl_id].user} or a master admin can kill this repl."
19
+ else
20
+ pids = `pgrep -P #{@run_repls[repl_id].pid}`.split("\n").map(&:to_i) #todo: it needs to be adapted for Windows
21
+ pids.each do |pd|
22
+ begin
23
+ Process.kill("KILL", pd)
24
+ rescue
25
+ end
26
+ end
27
+ respond "The repl #{@run_repls[repl_id].name} (id: #{repl_id}) has been killed."
28
+ @run_repls.delete(repl_id)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -121,6 +121,7 @@ class SlackSmartBot
121
121
  process_to_run = '
122
122
  ' + env_vars.join("\n") + '
123
123
  require \"amazing_print\"
124
+ require \"stringio\"
124
125
  bindme' + serialt + ' = binding
125
126
  eval(\"require \'nice_http\'\" , bindme' + serialt + ')
126
127
  def ls(obj)
@@ -2,28 +2,39 @@ class SlackSmartBot
2
2
  # help: ----------------------------------------------
3
3
  # help: `run repl SESSION_NAME`
4
4
  # help: `run repl SESSION_NAME ENV_VAR=VALUE ENV_VAR=VALUE`
5
+ # help: `run repl SESSION_NAME PARAMS`
5
6
  # help: `run live SESSION_NAME`
6
7
  # help: `run irb SESSION_NAME`
7
- # help: Will run the repl session specified and return the output.
8
+ # help: Will run the repl session specified and return the output.
8
9
  # help: You can supply the Environmental Variables you need for the Session
10
+ # help: PARAMS: Also it is possible to supply code that will be run before the repl code on the same session.
9
11
  # help: It will return only the values that were print out on the repl with puts, print, p or pp
10
12
  # help: Example:
11
13
  # help: _run repl CreateCustomer LOCATION=spain HOST='https://10.30.40.50:8887'_
12
14
  # help: <https://github.com/MarioRuiz/slack-smart-bot#repl|more info>
13
15
  # help: command_id: :run_repl
14
16
  # help:
15
- def run_repl(dest, user, session_name, env_vars, rules_file)
17
+ def run_repl(dest, user, session_name, env_vars, prerun, rules_file)
16
18
  #todo: add tests
17
19
  from = user.name
18
20
  if has_access?(__method__, user)
19
21
  save_stats(__method__)
20
22
  Dir.mkdir("#{config.path}/repl") unless Dir.exist?("#{config.path}/repl")
21
23
  Dir.mkdir("#{config.path}/repl/#{@channel_id}") unless Dir.exist?("#{config.path}/repl/#{@channel_id}")
24
+ code = prerun.join("\n")
22
25
  if File.exist?("#{config.path}/repl/#{@channel_id}/#{session_name}.run")
23
- if @repls.key?(session_name) and (@repls[session_name][:type] == :private or @repls[session_name][:type] == :private_clean) and
24
- @repls[session_name][:creator_name]!=user.name and
25
- !is_admin?(user.name)
26
+ if @repls.key?(session_name) and (@repls[session_name][:type] == :private or @repls[session_name][:type] == :private_clean) and
27
+ @repls[session_name][:creator_name] != user.name and
28
+ !is_admin?(user.name)
26
29
  respond "The REPL with session name: #{session_name} is private", dest
30
+ elsif !prerun.empty? and (code.match?(/System/i) or code.match?(/Kernel/i) or code.include?("File.") or
31
+ code.include?("`") or code.include?("exec") or code.include?("spawn") or code.include?("IO.") or
32
+ code.match?(/open3/i) or code.match?(/bundle/i) or code.match?(/gemfile/i) or code.include?("%x") or
33
+ code.include?("ENV") or code.match?(/=\s*IO/) or code.include?("Dir.") or
34
+ code.match?(/=\s*File/) or code.match?(/=\s*Dir/) or code.match?(/<\s*File/) or code.match?(/<\s*Dir/) or
35
+ code.match?(/\w+:\s*File/) or code.match?(/\w+:\s*Dir/) or
36
+ code.match?(/=?\s*(require|load)(\(|\s)/i))
37
+ respond "Sorry I cannot run this due security reasons", dest
27
38
  else
28
39
  if @repls.key?(session_name) #not temp
29
40
  @repls[session_name][:accessed] = Time.now.to_s
@@ -32,55 +43,124 @@ class SlackSmartBot
32
43
  else
33
44
  @repls[session_name][:runs_by_others] += 1
34
45
  end
35
- update_repls()
46
+ update_repls()
36
47
  end
37
48
 
38
49
  content = env_vars.join("\n")
39
50
  content += "\nrequire 'nice_http'\n"
40
51
  unless rules_file.empty? # to get the project_folder
41
52
  begin
42
- eval(File.new(config.path+rules_file).read) if File.exist?(config.path+rules_file)
53
+ eval(File.new(config.path + rules_file).read) if File.exist?(config.path + rules_file)
43
54
  end
44
55
  end
45
- if File.exist?("#{project_folder}/.smart-bot-repl") and
46
- ((@repls.key?(session_name) and @repls[session_name][:type] != :private_clean and @repls[session_name][:type] != :public_clean) or !@repls.key?(session_name))
56
+ if File.exist?("#{project_folder}/.smart-bot-repl") and
57
+ ((@repls.key?(session_name) and @repls[session_name][:type] != :private_clean and @repls[session_name][:type] != :public_clean) or !@repls.key?(session_name))
47
58
  content += File.read("#{project_folder}/.smart-bot-repl")
48
59
  content += "\n"
49
60
  end
50
- content += File.read("#{config.path}/repl/#{@channel_id}/#{session_name}.run").gsub(/^(quit|exit|bye)$/i,'') #todo: remove this gsub, it will never contain it
61
+ unless prerun.empty?
62
+ content += prerun.join("\n")
63
+ content += "\n"
64
+ end
65
+ content += File.read("#{config.path}/repl/#{@channel_id}/#{session_name}.run").gsub(/^(quit|exit|bye)$/i, "") #todo: remove this gsub, it will never contain it
51
66
  Dir.mkdir("#{project_folder}/tmp") unless Dir.exist?("#{project_folder}/tmp")
52
67
  Dir.mkdir("#{project_folder}/tmp/repl") unless Dir.exist?("#{project_folder}/tmp/repl")
53
- File.write("#{project_folder}/tmp/repl/#{session_name}.rb", content, mode: "w+")
54
- process_to_run = "ruby ./tmp/repl/#{session_name}.rb"
68
+ if Thread.current[:on_thread]
69
+ # to force stdout.each to be performed every 3 seconds
70
+ content = "Thread.new do
71
+ while true do
72
+ puts ''
73
+ sleep 3
74
+ end
75
+ end
76
+ #{content}
77
+ "
78
+ end
79
+ random = "5:LN&".gen
80
+ File.write("#{project_folder}/tmp/repl/#{session_name}_#{user.name}_#{random}.rb", content, mode: "w+")
81
+ process_to_run = "ruby ./tmp/repl/#{session_name}_#{user.name}_#{random}.rb"
55
82
  process_to_run = ("cd #{project_folder} && #{process_to_run}") if defined?(project_folder)
56
- respond "Running REPL #{session_name}"
57
- stdout, stderr, status = Open3.capture3(process_to_run)
58
- if stderr == ""
59
- if stdout == ""
60
- respond "*#{session_name}*: Nothing returned."
61
- else
62
- if stdout.to_s.lines.count < 60 and stdout.to_s.size < 3500
63
- output = ''
64
- stdout.each_line do |line|
65
- if line.match?(/^{.+}$/) or line.match?(/^\[.+\]$/)
66
- output += "```\n#{line}```\n"
83
+ respond "Running REPL #{session_name} (id: #{random})"
84
+ @run_repls[random] = { user: user.name, name: session_name, pid: '' }
85
+ react :running
86
+
87
+ require "pty"
88
+ timeout = 60 * 60 * 4 # 4 hours
89
+
90
+ started = Time.now
91
+ results = []
92
+ begin
93
+ PTY.spawn(process_to_run) do |stdout, stdin, pid|
94
+ last_result = -1
95
+ last_time = Time.now
96
+ @run_repls[random].pid = pid
97
+ begin
98
+ stdout.each do |line|
99
+ if (Time.now - started) > timeout
100
+ respond "run REPL session finished. Max time reached: #{session_name} (id: #{random})", dest
101
+ pids = `pgrep -P #{pid}`.split("\n").map(&:to_i) #todo: it needs to be adapted for Windows
102
+ pids.each do |pd|
103
+ begin
104
+ Process.kill("KILL", pd)
105
+ rescue
106
+ end
107
+ end
108
+ break
67
109
  else
68
- output +=line
110
+ results << line
111
+ if Thread.current[:on_thread]
112
+ if (Time.now - last_time) > 2
113
+ if (results.size - last_result) < 60 and results[(last_result + 1)..-1].join.size < 3500
114
+ output = ""
115
+ results[(last_result + 1)..-1].each do |li|
116
+ if li.match?(/^\s*{.+}\s*$/) or li.match?(/^\s*\[.+\]\s*$/)
117
+ output += "```\n#{li}```\n"
118
+ else
119
+ output += li
120
+ end
121
+ end
122
+ respond output
123
+ else
124
+ send_file(dest, "", "response.rb", "", "text/plain", "ruby", content: results[(last_result + 1)..-1].join)
125
+ end
126
+ last_result = results.size - 1
127
+ last_time = Time.now
128
+ end
129
+ end
69
130
  end
70
131
  end
71
- respond "*#{session_name}*: #{output}"
132
+ rescue Errno::EIO
133
+ @logger.warn "run_repl PTY Errno:EIO error"
134
+ end
135
+ if results.empty?
136
+ respond "*#{session_name}* (id: #{random}): Nothing returned."
72
137
  else
73
- send_file(dest, "", 'response.rb', "", 'text/plain', "ruby", content: stdout)
138
+ if last_result != (results.size - 1)
139
+ if (results.size - last_result) < 60 and results[(last_result + 1)..-1].join.size < 3500
140
+ output = ""
141
+ results[(last_result + 1)..-1].each do |li|
142
+ if li.match?(/^\s*{.+}\s*$/) or li.match?(/^\s*\[.+\]\s*$/)
143
+ output += "```\n#{li}```\n"
144
+ else
145
+ output += li
146
+ end
147
+ end
148
+ if Thread.current[:on_thread]
149
+ respond output
150
+ else
151
+ respond "*#{session_name}* (id: #{random}):\n#{output}"
152
+ end
153
+ else
154
+ send_file(dest, "", "response.rb", "", "text/plain", "ruby", content: results[(last_result + 1)..-1].join)
155
+ end
156
+ end
74
157
  end
75
158
  end
76
- else
77
- if (stdout.to_s+stderr.to_s).lines.count < 60
78
- respond "*#{session_name}*: #{stdout} #{stderr}"
79
- else
80
- send_file(dest, "", 'response.rb', "", 'text/plain', "ruby", content: (stdout.to_s+stderr.to_s))
81
- end
82
-
159
+ rescue PTY::ChildExited
160
+ @logger.warn "run_repl PTY The child process exited!"
83
161
  end
162
+ @run_repls.delete(random) if @run_repls.key?(random)
163
+ unreact :running
84
164
  end
85
165
  else
86
166
  respond "The REPL with session name: #{session_name} doesn't exist on this Smart Bot Channel", dest
@@ -2,6 +2,7 @@ class SlackSmartBot
2
2
  # helpmaster: ----------------------------------------------
3
3
  # helpmaster: `publish announcements`
4
4
  # helpmaster: It will publish on all channels the announcements added by using 'add announcement' command.
5
+ # helpmaster: It won't be published if less than 11 messages published on the channel since last time this command was called.
5
6
  # helpmaster: Only works if you are on Master channel and you are a master admin user
6
7
  # helpmaster: The messages stored on a DM won't be published.
7
8
  # helpmaster: This is very convenient to be called from a *Routine* for example every weekday at 09:00.
@@ -16,8 +17,9 @@ class SlackSmartBot
16
17
  channels.select! {|i| i[/\.csv$/]}
17
18
  channels.each do |channel|
18
19
  channel.gsub!('.csv','')
19
- unless channel[0]== 'D'
20
+ unless channel[0]== 'D' or (@announcements_activity_after.key?(channel) and @announcements_activity_after[channel] <= 10)
20
21
  see_announcements(user, '', channel, false, true)
22
+ @announcements_activity_after[channel] = 0
21
23
  sleep 0.5 # to avoid reach ratelimit
22
24
  end
23
25
  end
@@ -6,6 +6,7 @@ require_relative "commands/on_bot/ruby_code"
6
6
  require_relative "commands/on_bot/repl"
7
7
  require_relative "commands/on_bot/get_repl"
8
8
  require_relative "commands/on_bot/run_repl"
9
+ require_relative "commands/on_bot/kill_repl"
9
10
  require_relative "commands/on_bot/delete_repl"
10
11
  require_relative "commands/on_bot/see_repls"
11
12
  require_relative "commands/on_bot/general/whats_new"
@@ -59,7 +60,14 @@ require_relative "commands/general/see_access"
59
60
  require_relative "commands/general/allow_access"
60
61
  require_relative "commands/general/deny_access"
61
62
  require_relative "commands/general/add_team"
63
+ require_relative "commands/general/add_memo_team"
64
+ require_relative "commands/general/set_memo_status"
65
+ require_relative "commands/general/delete_memo_team"
62
66
  require_relative "commands/general/see_teams"
63
67
  require_relative "commands/general/update_team"
64
68
  require_relative "commands/general/ping_team"
65
69
  require_relative "commands/general/delete_team"
70
+ require_relative "commands/general/add_vacation"
71
+ require_relative "commands/general/remove_vacation"
72
+ require_relative "commands/general/see_vacations"
73
+ require_relative "commands/general/see_vacations_team"
@@ -152,7 +152,9 @@ class SlackSmartBot
152
152
  see_routines(dest, from, user, all)
153
153
  when /\A\s*get\s+bot\s+logs?\s*$/i
154
154
  get_bot_logs(dest, from, typem)
155
- when /\A\s*send\s+message\s+(on|to|in)\s*(.+)\s*:\s*(.+)\s*$/im
155
+ when /\A\s*send\s+message\s+(on|to|in)\s+<(https?:[^:]+)>\s*:\s*(.+)\s*$/im,
156
+ /\A\s*send\s+message\s+(on|to|in)\s+(https?:[^:]+)\s*:\s*(.+)\s*$/im,
157
+ /\A\s*send\s+message\s+(on|to|in)\s*([^:]+)\s*:\s*(.+)\s*$/im
156
158
  opts = $2
157
159
  message = $3
158
160
  thread_ts = ''
@@ -174,7 +176,7 @@ class SlackSmartBot
174
176
 
175
177
  thread_ts.gsub!('.','')
176
178
  send_message(dest, from, typem, to, thread_ts, message)
177
- when /\A\s*delete\s+message\s+(.+)\s*$/i
179
+ when /\A\s*delete\s+message\s+(http.+)\s*$/i
178
180
  url = $1
179
181
  delete_message(from, typem, url)
180
182
  when /\A\s*react\s+(on|to|in)\s*([^\s]+)\s+([p\d\.]+)\s+(.+)\s*$/i,
@@ -396,11 +398,15 @@ class SlackSmartBot
396
398
  when /\A\s*get\s+(repl|irb|live)\s+([\w\-]+)\s*/i
397
399
  session_name = $2
398
400
  get_repl(dest, user, session_name)
399
- when /\A\s*run\s+(repl|irb|live)\s+([\w\-]+)()\s*$/i,
400
- /^\s*run\s+(repl|irb|live)\s+([\w\-]+)\s+(.+)\s*$/i
401
+ when /\A\s*run\s+(repl|irb|live)\s+([\w\-]+)()\s*\z/im,
402
+ /^\s*run\s+(repl|irb|live)\s+([\w\-]+)\s+(.+)\s*$/im
401
403
  session_name = $2
402
- opts = " #{$3}"
403
- env_vars = opts.scan(/\s+[\w\-]+="[^"]+"/i) + opts.scan(/\s+[\w\-]+='[^']+'/i)
404
+ if Thread.current[:command_orig].match(/\s*run\s+(repl|irb|live)\s+([\w\-]+)\s+(.+)\s*$/im)
405
+ opts = " #{$3}"
406
+ else
407
+ opts = ''
408
+ end
409
+ env_vars = opts.scan(/\s+[\w\-]+="[^"]+"/i) + opts.scan(/\s+[\w\-]+='[^']+'/i)
404
410
  opts.scan(/\s+[\w\-]+=[^'"\s]+/i).flatten.each do |ev|
405
411
  env_vars << ev.gsub('=',"='") + "'"
406
412
  end
@@ -409,12 +415,16 @@ class SlackSmartBot
409
415
  ev.lstrip!
410
416
  env_vars[idx] = "ENV['#{ev}"
411
417
  end
412
- run_repl(dest, user, session_name, env_vars.flatten, rules_file)
418
+ prerun = Thread.current[:command_orig].gsub('```', '`').scan(/\s+`(.+)`/m)
419
+ run_repl(dest, user, session_name, env_vars.flatten, prerun.flatten, rules_file)
413
420
  when /\A\s*(delete|remove)\s+(repl|irb|live)\s+([\w\-]+)\s*$/i
414
421
  repl_name = $3
415
422
  delete_repl(dest, user, repl_name)
416
423
  when /\A\s*see\s+(repls|repl|irb|irbs)\s*$/i
417
424
  see_repls(dest, user, typem)
425
+ when /\A\s*(kill)\s+(repl|irb|live)\s+([\w]+)\s*$/i
426
+ repl_id = $3
427
+ kill_repl(dest, user, repl_id)
418
428
  else
419
429
  processed2 = false
420
430
  end #of case
@@ -74,6 +74,12 @@ class SlackSmartBot
74
74
  if !dest.nil? and config.on_master_bot and !data.text.nil? and data.text.match(/^ping from (.+)\s*$/) and data.user == config[:nick_id]
75
75
  @pings << $1
76
76
  end
77
+ if config.on_master_bot and @vacations_check != Date.today
78
+ @vacations_check = Date.today
79
+ t = Thread.new do
80
+ check_vacations(only_first_day: true)
81
+ end
82
+ end
77
83
  typem = :dont_treat
78
84
  if data.nil? or data.user.nil? or data.user.to_s==''
79
85
  user_info = nil
@@ -186,7 +192,7 @@ class SlackSmartBot
186
192
  end
187
193
  end
188
194
  load "#{config.path}/rules/general_commands.rb" if File.exist?("#{config.path}/rules/general_commands.rb") and @datetime_general_commands != File.mtime("#{config.path}/rules/general_commands.rb")
189
-
195
+ eval(File.new(config.path + config.rules_file).read) if !defined?(rules) and File.exist?(config.path+config.rules_file) and !config.rules_file.empty?
190
196
  unless typem == :dont_treat or user_info.nil?
191
197
  if (Time.now - @last_activity_check) > 60 * 30 #every 30 minutes
192
198
  @last_activity_check = Time.now
@@ -350,6 +356,10 @@ class SlackSmartBot
350
356
  end
351
357
  end
352
358
  end
359
+ unless data.nil? or data.channel.nil? or data.channel.empty?
360
+ @announcements_activity_after[data.channel] ||= 0
361
+ @announcements_activity_after[data.channel] += 1
362
+ end
353
363
  rescue Exception => stack
354
364
  @logger.fatal stack
355
365
  end