slack-smart-bot 1.15.0 → 1.15.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/img/chat_gpt_attach_image.png +0 -0
  4. data/lib/slack/smart-bot/ai/open_ai/models.rb +19 -10
  5. data/lib/slack/smart-bot/ai/open_ai/send_gpt_chat.rb +14 -10
  6. data/lib/slack/smart-bot/comm/dont_understand.rb +23 -6
  7. data/lib/slack/smart-bot/comm/get_user_info.rb +9 -10
  8. data/lib/slack/smart-bot/comm/respond.rb +56 -28
  9. data/lib/slack/smart-bot/comm/send_file.rb +17 -6
  10. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat.rb +885 -129
  11. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_add_collaborator.rb +3 -3
  12. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_copy_session.rb +132 -15
  13. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_delete_session.rb +1 -1
  14. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_get_prompts.rb +50 -12
  15. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_list_sessions.rb +99 -34
  16. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_share_session.rb +12 -2
  17. data/lib/slack/smart-bot/commands/general/ai/open_ai/open_ai_chat_use_model.rb +36 -25
  18. data/lib/slack/smart-bot/commands/general/bot_help.rb +29 -24
  19. data/lib/slack/smart-bot/commands/general/poster.rb +0 -1
  20. data/lib/slack/smart-bot/commands/general/see_announcements.rb +1 -1
  21. data/lib/slack/smart-bot/commands/general/summarize.rb +22 -8
  22. data/lib/slack/smart-bot/commands/general_bot_commands.rb +156 -55
  23. data/lib/slack/smart-bot/commands/on_bot/general/bot_stats.rb +13 -11
  24. data/lib/slack/smart-bot/commands/on_extended/bot_rules.rb +21 -17
  25. data/lib/slack/smart-bot/commands/on_master/admin_master/exit_bot.rb +2 -2
  26. data/lib/slack/smart-bot/commands.rb +19 -19
  27. data/lib/slack/smart-bot/process_first.rb +38 -35
  28. data/lib/slack/smart-bot/treat_message.rb +57 -56
  29. data/lib/slack/smart-bot/utils/download_http_content.rb +91 -0
  30. data/lib/slack/smart-bot/utils/get_authorizations.rb +41 -0
  31. data/lib/slack/smart-bot/utils/get_keywords.rb +33 -0
  32. data/lib/slack/smart-bot/utils/get_openai_sessions.rb +46 -6
  33. data/lib/slack/smart-bot/utils/get_teams.rb +9 -1
  34. data/lib/slack/smart-bot/utils/save_stats.rb +13 -5
  35. data/lib/slack/smart-bot/utils/transform_to_slack_markdown.rb +36 -0
  36. data/lib/slack/smart-bot/utils/update_openai_sessions.rb +9 -4
  37. data/lib/slack/smart-bot/utils.rb +48 -44
  38. data/lib/slack-smart-bot.rb +10 -10
  39. data/whats_new.txt +27 -1
  40. metadata +49 -8
@@ -7,42 +7,42 @@ class SlackSmartBot
7
7
  unless data.text.to_s.match(/\A\s*\z/)
8
8
  #to remove italic, bold... from data.text since there is no method on slack api
9
9
  if remove_blocks and !data.blocks.nil? and data.blocks.size > 0
10
- data_text = ''
10
+ data_text = ""
11
11
  data.blocks.each do |b|
12
- if b.type == 'rich_text'
12
+ if b.type == "rich_text"
13
13
  if b.elements.size > 0
14
14
  b.elements.each do |e|
15
- if e.type == 'rich_text_section' or e.type == 'rich_text_preformatted'
16
- if e.elements.size > 0 and (e.elements.type.uniq - ['link', 'text', 'user', 'channel']) == []
17
- data_text += '```' if e.type == 'rich_text_preformatted'
15
+ if e.type == "rich_text_section" or e.type == "rich_text_preformatted"
16
+ if e.elements.size > 0 and (e.elements.type.uniq - ["link", "text", "user", "channel"]) == []
17
+ data_text += "```" if e.type == "rich_text_preformatted"
18
18
  e.elements.each do |el|
19
- if el.type == 'text'
19
+ if el.type == "text"
20
20
  data_text += el.text
21
- elsif el.type == 'user'
21
+ elsif el.type == "user"
22
22
  data_text += "<@#{el.user_id}>"
23
- elsif el.type == 'channel'
23
+ elsif el.type == "channel"
24
24
  tch = data.text.scan(/(<##{el.channel_id}\|[^\>]*>)/).flatten.first
25
25
  data_text += tch.to_s
26
26
  else
27
27
  data_text += el.url
28
28
  end
29
29
  end
30
- data_text += '```' if e.type == 'rich_text_preformatted'
30
+ data_text += "```" if e.type == "rich_text_preformatted"
31
31
  end
32
32
  end
33
33
  end
34
34
  end
35
35
  end
36
36
  end
37
- data.text = data_text unless data_text == ''
37
+ data.text = data_text unless data_text == ""
38
38
  end
39
39
  data.text = CGI.unescapeHTML(data.text)
40
40
  data.text.gsub!("\u00A0", " ") #to change &nbsp; (asc char 160) into blank space
41
41
  end
42
- data.text.gsub!('', "'")
43
- data.text.gsub!('', "'")
44
- data.text.gsub!('', '"')
45
- data.text.gsub!('', '"')
42
+ data.text.gsub!("", "'")
43
+ data.text.gsub!("", "'")
44
+ data.text.gsub!("", '"')
45
+ data.text.gsub!("", '"')
46
46
  rescue Exception => exc
47
47
  @logger.warn "Impossible to unescape or clean format for data.text:#{data.text}"
48
48
  @logger.warn exc.inspect
@@ -50,8 +50,8 @@ class SlackSmartBot
50
50
 
51
51
  unless data.key?(:routine)
52
52
  data.routine = false
53
- data.routine_name = ''
54
- data.routine_type = ''
53
+ data.routine_name = ""
54
+ data.routine_type = ""
55
55
  end
56
56
  if config[:testing] and config.on_master_bot and !@buffered
57
57
  @buffered = true
@@ -59,7 +59,8 @@ class SlackSmartBot
59
59
  f.puts "|#{data.channel}|#{data.thread_ts}|#{data.user}|#{data.user_name}|#{data.text}"
60
60
  }
61
61
  end
62
- if data.key?(:dest) and data.dest.to_s!='' # for run routines and publish on different channels
62
+
63
+ if data.key?(:dest) and data.dest.to_s != "" # for run routines and publish on different channels
63
64
  dest = data.dest
64
65
  elsif data.channel[0] == "D" or data.channel[0] == "C" or data.channel[0] == "G" #Direct message or Channel or Private Channel
65
66
  dest = data.channel
@@ -73,9 +74,9 @@ class SlackSmartBot
73
74
  end
74
75
 
75
76
  #open ai chat gpt and shared messages as an input
76
- if data.text.match?(/\A\s*(^|!!|!)?\s*(\?|\?\?)\s*/im) and !data.attachments.nil? and data.attachments.size > 0 and !data.attachments[0].text.nil? and data.attachments[0].text != ''
77
+ if data.text.match?(/\A\s*(^|!!|!)?\s*(\?|\?\?)\s*/im) and !data.attachments.nil? and data.attachments.size > 0 and !data.attachments[0].text.nil? and data.attachments[0].text != ""
77
78
  data.attachments.each_with_index do |att, i|
78
- if !att.text.nil? and att.text != ''
79
+ if !att.text.nil? and att.text != ""
79
80
  data.text += "\n#{att.text}"
80
81
  end
81
82
  end
@@ -83,6 +84,7 @@ class SlackSmartBot
83
84
  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]
84
85
  @pings << $1
85
86
  end
87
+
86
88
  if config.on_master_bot and @vacations_check != Time.now.strftime("%Y%m%d%H") #every hour since depends on user's time zone
87
89
  @vacations_check = Time.now.strftime("%Y%m%d%H")
88
90
  t = Thread.new do
@@ -92,19 +94,22 @@ class SlackSmartBot
92
94
  typem = :dont_treat
93
95
 
94
96
  @users = get_users() if @users.empty?
95
- if data.key?(:bot_id) and data.bot_id.to_s!=''
97
+ if data.key?(:bot_id) and data.bot_id.to_s != ""
96
98
  if @slack_bots.key?(data.bot_id) #bot or workflow
97
99
  data.user = @slack_bots[data.bot_id]
98
100
  else
99
101
  bot_info = get_user_info(data.bot_id, is_bot: true)
100
- unless bot_info.nil? or bot_info.empty?
101
- @slack_bots[data.bot_id] = bot_info.user.id
102
- data.user = bot_info.user.id
103
- @users << bot_info.user unless bot_info.nil? or bot_info.empty?
102
+ if bot_info.nil? or bot_info.empty?
103
+ @logger.warn "Bot not found on users with data: #{data.inspect}"
104
+ else
105
+ @slack_bots[data.bot_id] = bot_info.user.id
106
+ data.user = bot_info.user.id
107
+ @users << bot_info.user unless bot_info.nil? or bot_info.empty?
104
108
  end
105
109
  end
106
110
  end
107
- if data.nil? or data.user.nil? or data.user.to_s==''
111
+
112
+ if data.nil? or data.user.nil? or data.user.to_s == ""
108
113
  user_info = nil
109
114
  else
110
115
  user_info = find_user(data.user, get_sso_user_name: true)
@@ -115,27 +120,26 @@ class SlackSmartBot
115
120
  end
116
121
  end
117
122
  if !dest.nil? and !data.text.nil? and !data.text.to_s.match?(/\A\s*\z/)
118
-
119
123
  get_bots_created()
120
124
  if data.channel[0] == "D" and !data.text.to_s.match?(/^\s*<@#{config[:nick_id]}>\s+/) and
121
- (data.text.to_s.match?(/^\s*(on)?\s*<#\w+\|[^>]*>/i) or data.text.to_s.match?(/^\s*(on)?\s*#\w+/i))
125
+ (data.text.to_s.match?(/^\s*(on)?\s*<#\w+\|[^>]*>/i) or data.text.to_s.match?(/^\s*(on)?\s*#\w+/i))
122
126
  data.text = "<@#{config[:nick_id]}> " + data.text.to_s
123
127
  end
124
128
  #todo: we need to add mixed channels: @smart-bot on private1 #bot1cm <#CXDDFRDDF|bot2cu>: echo A
125
129
  if data.text.match(/\A\^\^+/) # to open a thread it will be only when starting by single ^
126
130
  typem = :dont_treat
127
131
  elsif data.text.match(/^\s*<@#{config[:nick_id]}>\s+(on\s+)?((<#\w+\|[^>]*>\s*)+)\s*:?\s*(.*)/im) or
128
- data.text.match(/^\s*<@#{config[:nick_id]}>\s+(on\s+)?((#[a-zA-Z0-9\-\_]+\s*)+)\s*:?\s*(.*)/im) or
129
- data.text.match(/^\s*<@#{config[:nick_id]}>\s+(on\s+)?(([a-zA-Z0-9\-\_]+\s*)+)\s*:\s*(.*)/im)
132
+ data.text.match(/^\s*<@#{config[:nick_id]}>\s+(on\s+)?((#[a-zA-Z0-9\-\_]+\s*)+)\s*:?\s*(.*)/im) or
133
+ data.text.match(/^\s*<@#{config[:nick_id]}>\s+(on\s+)?(([a-zA-Z0-9\-\_]+\s*)+)\s*:\s*(.*)/im)
130
134
  channels_rules = $2 #multiple channels @smart-bot on #channel1 #channel2 echo AAA
131
135
  data_text = $4
132
- channel_rules_name = ''
133
- channel_rules = ''
136
+ channel_rules_name = ""
137
+ channel_rules = ""
134
138
  channels_arr = channels_rules.scan(/<#(\w+)\|([^>]*)>/)
135
139
  if channels_arr.size == 0
136
140
  channels_arr = []
137
141
  channels_rules.scan(/([^\s]+)/).each do |cn|
138
- cna = cn.join.gsub('#','')
142
+ cna = cn.join.gsub("#", "")
139
143
  if @channels_name.key?(cna)
140
144
  channels_arr << [cna, @channels_name[cna]]
141
145
  else
@@ -144,8 +148,8 @@ class SlackSmartBot
144
148
  end
145
149
  else
146
150
  channels_arr.each do |row|
147
- row[0] = @channels_id[row[1]] if row[0] == ''
148
- row[1] = @channels_name[row[0]] if row[1] == ''
151
+ row[0] = @channels_id[row[1]] if row[0] == ""
152
+ row[1] = @channels_name[row[0]] if row[1] == ""
149
153
  end
150
154
  end
151
155
 
@@ -165,7 +169,6 @@ class SlackSmartBot
165
169
  break
166
170
  end
167
171
  end
168
-
169
172
  elsif data.channel == @master_bot_id
170
173
  if config.on_master_bot #only to be treated on master bot channel
171
174
  typem = :on_master
@@ -177,7 +180,7 @@ class SlackSmartBot
177
180
  elsif data.channel[0] == "D" #Direct message
178
181
  get_rules_imported()
179
182
  if @rules_imported.key?("#{user_info.team_id}_#{user_info.name}") && @rules_imported["#{user_info.team_id}_#{user_info.name}"].key?(user_info.name) and
180
- @bots_created.key?(@rules_imported["#{user_info.team_id}_#{user_info.name}"][user_info.name])
183
+ @bots_created.key?(@rules_imported["#{user_info.team_id}_#{user_info.name}"][user_info.name])
181
184
  if @channel_id == @rules_imported["#{user_info.team_id}_#{user_info.name}"][user_info.name]
182
185
  #only to be treated by the channel we are 'using'
183
186
  typem = :on_dm
@@ -189,9 +192,9 @@ class SlackSmartBot
189
192
  elsif data.channel[0] == "C" or data.channel[0] == "G"
190
193
  #only to be treated on the channel of the bot. excluding running ruby
191
194
  if !config.on_master_bot and @bots_created.key?(@channel_id) and @bots_created[@channel_id][:extended].include?(@channels_name[data.channel]) and
192
- !data.text.match?(/^!?\s*(ruby|code)\s+/) and !data.text.match?(/^!?!?\s*(ruby|code)\s+/) and !data.text.match?(/^\^?\s*(ruby|code)\s+/)
195
+ !data.text.match?(/^!?\s*(ruby|code)\s+/) and !data.text.match?(/^!?!?\s*(ruby|code)\s+/) and !data.text.match?(/^\^?\s*(ruby|code)\s+/)
193
196
  typem = :on_extended
194
- elsif config.on_master_bot and (data.text.match?(/^!?\s*(ruby|code)\s+/) or data.text.match?(/^!?!?\s*(ruby|code)\s+/) or data.text.match?(/^\^?\s*(ruby|code)\s+/) )
197
+ elsif config.on_master_bot and (data.text.match?(/^!?\s*(ruby|code)\s+/) or data.text.match?(/^!?!?\s*(ruby|code)\s+/) or data.text.match?(/^\^?\s*(ruby|code)\s+/))
195
198
  #or in case of running ruby, the master bot
196
199
  @bots_created.each do |k, v|
197
200
  if v.key?(:extended) and v[:extended].include?(@channels_name[data.channel])
@@ -209,25 +212,25 @@ class SlackSmartBot
209
212
  end
210
213
  if data.channel[0] == "G" and config.on_master_bot and !extended #private group
211
214
  typem = :on_pg
212
- elsif data.channel[0] == 'C' and config.on_master_bot and !extended #public group
215
+ elsif data.channel[0] == "C" and config.on_master_bot and !extended #public group
213
216
  typem = :on_pub
214
217
  end
215
218
  end
216
219
  end
217
220
  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")
218
- 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?
221
+ 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?
219
222
  unless typem == :dont_treat or user_info.nil?
220
223
  if (Time.now - @last_activity_check) > TIMEOUT_LISTENING #every 30 minutes
221
224
  @last_activity_check = Time.now
222
- @listening.each do |k,v|
225
+ @listening.each do |k, v|
223
226
  unless k == :threads
224
227
  v.each do |kk, vv|
225
228
  if (Time.now - vv) > TIMEOUT_LISTENING
226
229
  if @listening[:threads].key?(kk) && @active_chat_gpt_sessions.key?(k) &&
227
- @active_chat_gpt_sessions[k].key?(kk)
230
+ @active_chat_gpt_sessions[k].key?(kk)
228
231
  unreact :running, kk, channel: @listening[:threads][kk]
229
232
  session_name = @active_chat_gpt_sessions[k][kk]
230
- chatgpt_message = "ChatGPT session has been terminated due to inactivity."
233
+ chatgpt_message = ":information_source: ChatGPT session has been terminated due to inactivity."
231
234
  if !session_name.to_s.empty?
232
235
  chatgpt_message += "\n\nIf you want to start it again on this thread call `chatgpt #{session_name}`"
233
236
  end
@@ -259,16 +262,15 @@ class SlackSmartBot
259
262
  @answer[team_id_user][qdest] = data.text
260
263
  @questions[team_id_user] = data.text # to be backwards compatible #todo remove it when 2.0
261
264
  end
262
- elsif @repl_sessions.key?(team_id_user) and data.channel==@repl_sessions[team_id_user][:dest] and
263
- ((@repl_sessions[team_id_user][:on_thread] and data.thread_ts == @repl_sessions[team_id_user][:thread_ts]) or
264
- (!@repl_sessions[team_id_user][:on_thread] and data.thread_ts.to_s == '' ))
265
-
265
+ elsif @repl_sessions.key?(team_id_user) and data.channel == @repl_sessions[team_id_user][:dest] and
266
+ ((@repl_sessions[team_id_user][:on_thread] and data.thread_ts == @repl_sessions[team_id_user][:thread_ts]) or
267
+ (!@repl_sessions[team_id_user][:on_thread] and data.thread_ts.to_s == ""))
266
268
  if data.text.match(/^\s*```(.*)```\s*$/im)
267
- @repl_sessions[team_id_user][:command] = $1
269
+ @repl_sessions[team_id_user][:command] = $1
268
270
  else
269
271
  @repl_sessions[team_id_user][:command] = data.text
270
272
  end
271
- command = 'repl'
273
+ command = "repl"
272
274
  else
273
275
  command = data.text
274
276
  end
@@ -276,13 +278,13 @@ class SlackSmartBot
276
278
  if command.match(/\A\s*```(.*)```\s*\z/im)
277
279
  command = $1
278
280
  elsif command.size >= 2 and
279
- ((command[0] == "`" and command[-1] == "`") or (command[0] == "*" and command[-1] == "*") or (command[0] == "_" and command[-1] == "_"))
281
+ ((command[0] == "`" and command[-1] == "`") or (command[0] == "*" and command[-1] == "*") or (command[0] == "_" and command[-1] == "_"))
280
282
  command = command[1..-2]
281
283
  end
282
284
 
283
285
  #ruby file attached
284
286
  if !data.files.nil? and data.files.size == 1 and
285
- (command.match?(/^(ruby|code)\s*$/) or (command.match?(/^\s*$/) and data.files[0].filetype == "ruby") or
287
+ (command.match?(/^(ruby|code)\s*$/) or (command.match?(/^\s*$/) and data.files[0].filetype == "ruby") or
286
288
  (typem == :on_call and data.files[0].filetype == "ruby"))
287
289
  res = Faraday.new("https://files.slack.com", headers: { "Authorization" => "Bearer #{config[:token]}" }).get(data.files[0].url_private)
288
290
  command += " ruby" if command != "ruby"
@@ -326,11 +328,11 @@ class SlackSmartBot
326
328
  @logger.fatal stack
327
329
  end
328
330
  else
329
- if user_info.nil? and data.user.to_s!=''
331
+ if user_info.nil? and data.user.to_s != ""
330
332
  @logger.warn "Pay attention there is no user on users with id #{data.user}"
331
333
  end
332
334
  if !config.on_master_bot and !dest.nil? and (data.channel == @master_bot_id or dest[0] == "D") and
333
- data.text.match?(/^\s*(!|!!|\^)?\s*bot\s+status\s*$/i) and @admin_users_id.include?(data.user)
335
+ data.text.match?(/^\s*(!|!!|\^)?\s*bot\s+status\s*$/i) and @admin_users_id.include?(data.user)
334
336
  respond "ping from #{config.channel}", dest
335
337
  elsif !config.on_master_bot and !dest.nil? and data.user == config[:nick_id] and dest == @master_bot_id
336
338
  # to treat on other bots the status messages populated on master bot
@@ -370,7 +372,6 @@ class SlackSmartBot
370
372
  end
371
373
  end
372
374
  end
373
-
374
375
  when /^Bot has been (closed|killed) by/i
375
376
  sleep 2
376
377
  get_bots_created()
@@ -409,9 +410,9 @@ class SlackSmartBot
409
410
  if @status == :exit
410
411
  @listening[:threads].each do |thread_ts, channel_thread|
411
412
  unreact :running, thread_ts, channel: channel_thread
412
- respond "ChatGPT session closed since SmartBot is going to be closed", channel_thread, thread_ts: thread_ts
413
+ respond "ChatGPT session closed since SmartBot is going to be closed.\nCheck <##{@channels_id[config.status_channel]}>", channel_thread, thread_ts: thread_ts
413
414
  end
414
- @logger.info 'Game over!'
415
+ @logger.info "Game over!"
415
416
  sleep 3
416
417
  exit!
417
418
  end
@@ -0,0 +1,91 @@
1
+ class SlackSmartBot
2
+ def download_http_content(url, authorizations, team_id_user_creator = nil, session_name = nil)
3
+ begin
4
+ url_message = ""
5
+ parsed_url = URI.parse(url)
6
+
7
+ headers = {}
8
+
9
+ authorizations.each do |key, value|
10
+ if key.match?(/^(https:\/\/|http:\/\/)?#{parsed_url.host.gsub(".", '\.')}/)
11
+ value.each do |k, v|
12
+ headers[k.to_sym] = v unless k == :host
13
+ end
14
+ end
15
+ end
16
+
17
+ extra_message = ""
18
+ domain = "#{parsed_url.scheme}://#{parsed_url.host}"
19
+ if parsed_url.host.match?(/(drive|docs)\.google\.com/) #download the file
20
+ if url.include?("/file/d/")
21
+ gdrive_id = url.split("/d/")[1].split("/")[0]
22
+ url = "https://drive.google.com/uc?id=#{gdrive_id}&export=download"
23
+ end
24
+ io = URI.open(url, headers)
25
+ is_pdf = io.meta["content-type"].to_s == "application/pdf" || io.meta["content-disposition"].to_s.include?("pdf")
26
+ if is_pdf
27
+ require "pdf-reader"
28
+ if io.meta["content-disposition"].to_s.include?("pdf")
29
+ pdf_filename = io.meta["content-disposition"].split("filename=")[1].strip
30
+ extra_message = " PDF file: #{pdf_filename}"
31
+ end
32
+ reader = PDF::Reader.new(io)
33
+ text = reader.pages.map(&:text).join("\n")
34
+ else
35
+ is_docx = io.meta["content-type"].to_s == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || io.meta["content-disposition"].to_s.include?("docx")
36
+ if is_docx
37
+ require "docx"
38
+ if io.meta["content-disposition"].to_s.include?("docx")
39
+ docx_filename = io.meta["content-disposition"].split("filename=")[1].strip
40
+ extra_message = " DOCX file: #{docx_filename}"
41
+ end
42
+ doc = Docx::Document.open(io)
43
+ text = doc.paragraphs.map(&:to_s).join("\n")
44
+ else #text
45
+ text = io.read
46
+ text_filename = io.meta["content-disposition"].split("filename=")[1].strip if io.meta["content-disposition"].to_s.include?("filename=")
47
+ extra_message = " Text file: #{text_filename}"
48
+ end
49
+ end
50
+ io.close
51
+ elsif parsed_url.path.match?(/\.pdf$/)
52
+ require "pdf-reader"
53
+ io = URI.open(url, headers)
54
+ reader = PDF::Reader.new(io)
55
+ text = reader.pages.map(&:text).join("\n")
56
+ io.close
57
+ elsif parsed_url.path.match?(/\.docx?$/)
58
+ require "docx"
59
+ io = URI.open(url, headers)
60
+ doc = Docx::Document.open(io)
61
+ text = doc.paragraphs.map(&:to_s).join("\n")
62
+ io.close
63
+ else
64
+ parsed_url += "/" if parsed_url.path == ""
65
+ http = NiceHttp.new(host: domain, headers: headers, log: :no)
66
+ path = parsed_url.path
67
+ path += "?#{parsed_url.query}" if parsed_url.query
68
+ response = http.get(path)
69
+ html_doc = Nokogiri::HTML(response.body)
70
+ html_doc.search("script, style").remove
71
+ text = html_doc.text.strip
72
+ text.gsub!(/^\s*$/m, "")
73
+ http.close
74
+ end
75
+ if !session_name.nil? and !team_id_user_creator.nil?
76
+ if (!@open_ai[team_id_user_creator][:chat_gpt][:sessions][session_name].key?(:live_content) or
77
+ @open_ai[team_id_user_creator][:chat_gpt][:sessions][session_name][:live_content].nil? or
78
+ !@open_ai[team_id_user_creator][:chat_gpt][:sessions][session_name][:live_content].include?(url)) and
79
+ (!@open_ai[team_id_user_creator][:chat_gpt][:sessions][session_name].key?(:static_content) or
80
+ @open_ai[team_id_user_creator][:chat_gpt][:sessions][session_name][:static_content].nil? or
81
+ !@open_ai[team_id_user_creator][:chat_gpt][:sessions][session_name][:static_content].include?(url))
82
+ url_message = "> #{url}#{extra_message}: content extracted and added to prompt"
83
+ end
84
+ end
85
+ rescue Exception => e
86
+ text = "Error: #{e.message}"
87
+ url_message = "> #{url}: #{text}\n"
88
+ end
89
+ return text, url_message
90
+ end
91
+ end
@@ -0,0 +1,41 @@
1
+ class SlackSmartBot
2
+ def get_authorizations(session_name, team_id_user_creator)
3
+ team_id_user = Thread.current[:team_id_user]
4
+
5
+ authorizations = {}
6
+
7
+ if config.key?(:authorizations)
8
+ config[:authorizations].each do |key, value|
9
+ if value.key?(:host)
10
+ authorizations[value[:host]] = value
11
+ end
12
+ end
13
+ end
14
+
15
+ if @personal_settings_hash.key?(team_id_user) and @personal_settings_hash[team_id_user].key?(:authorizations)
16
+ @personal_settings_hash[team_id_user][:authorizations].each do |key, value|
17
+ if value.key?(:host)
18
+ authorizations[value[:host]] = value
19
+ end
20
+ end
21
+ end
22
+
23
+ if !session_name.nil?
24
+ if team_id_user != team_id_user_creator
25
+ team_id_to_use = team_id_user_creator
26
+ else
27
+ team_id_to_use = team_id_user
28
+ end
29
+ if @open_ai[team_id_to_use][:chat_gpt][:sessions].key?(session_name) and
30
+ @open_ai[team_id_to_use][:chat_gpt][:sessions][session_name].key?(:authorizations) and
31
+ !@open_ai[team_id_to_use][:chat_gpt][:sessions][session_name][:authorizations].nil?
32
+ @open_ai[team_id_to_use][:chat_gpt][:sessions][session_name][:authorizations].each do |host, header|
33
+ authorizations[host] ||= {}
34
+ authorizations[host].merge!(header)
35
+ end
36
+ end
37
+ end
38
+
39
+ return authorizations
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ class SlackSmartBot
2
+ def get_keywords(sentence, list_avoid: [])
3
+ require "engtagger"
4
+ keywords = []
5
+ unless sentence.to_s.strip.empty?
6
+ # Initialize the POS tagger
7
+ tagger = EngTagger.new
8
+ tagged_sentence = tagger.add_tags(sentence)
9
+ unless tagged_sentence.nil?
10
+
11
+ # Extract nouns and proper nouns from the sentence
12
+ nouns = tagger.get_nouns(tagged_sentence).keys
13
+ proper_nouns = tagger.get_proper_nouns(tagged_sentence).keys
14
+ adjectives = tagger.get_adjectives(tagged_sentence).keys
15
+ ids = sentence.scan(/([\w]+\-[\w\-]+)/)
16
+
17
+ # Combine nouns and proper nouns to create the list of keywords
18
+ keywords = (nouns + proper_nouns + adjectives + ids.flatten).uniq
19
+
20
+ #delete all keywords that are one or two characters long
21
+ keywords.delete_if { |keyword| keyword.length < 3 }
22
+ # delete all keywords that are in the list_avoid /word/i
23
+ if !list_avoid.empty?
24
+ keywords.delete_if { |keyword| list_avoid.any? { |avoid| keyword.match?(/#{avoid}/i) } }
25
+ end
26
+
27
+ #remove special characters from the keywords
28
+ keywords.map! { |keyword| keyword.gsub(/[^\w\-_]/i, "") }
29
+ end
30
+ end
31
+ return keywords
32
+ end
33
+ end
@@ -15,21 +15,21 @@ class SlackSmartBot
15
15
  files += Dir.glob(File.join(folder, "o_*.yaml"))
16
16
  end
17
17
  @datetime_open_ai_file ||= {}
18
-
18
+
19
19
  files.each do |file|
20
20
  if !defined?(@datetime_open_ai_file) or !@datetime_open_ai_file.key?(file) or @datetime_open_ai_file[file] != File.mtime(file)
21
- opean_ai_user = YAML.load(Utils::Encryption.decrypt(File.read(file), config))
21
+ open_ai_user = YAML.load(Utils::Encryption.decrypt(File.read(file), config))
22
22
  #user_file will be the team_id _ + the user name
23
23
  user_team_id = File.basename(File.dirname(file))
24
24
  user_file = user_team_id + "_" + File.basename(file).gsub("o_","").gsub(".yaml","")
25
-
25
+
26
26
  if @open_ai.key?(user_file) and @open_ai[user_file].key?(:chat_gpt) and @open_ai[user_file][:chat_gpt].key?(:sessions) and
27
27
  @open_ai[user_file][:chat_gpt][:sessions].key?("")
28
- temp_session = @open_ai[user_file][:chat_gpt][:sessions][""].deep_copy
28
+ temp_session = @open_ai[user_file][:chat_gpt][:sessions][""].deep_copy
29
29
  else
30
30
  temp_session = nil
31
31
  end
32
- @open_ai[user_file] = opean_ai_user
32
+ @open_ai[user_file] = open_ai_user
33
33
  @open_ai[user_file][:chat_gpt][:sessions][""] = temp_session unless temp_session.nil?
34
34
  @datetime_open_ai_file[file] = File.mtime(file)
35
35
  end
@@ -40,7 +40,47 @@ class SlackSmartBot
40
40
  @ai_gpt ||= {}
41
41
  @ai_gpt[team_id_user] ||= {}
42
42
  content = File.read(file_name)
43
- @ai_gpt[team_id_user][session_name] = Utils::Encryption.decrypt(content, config).force_encoding("UTF-8").split("\n")
43
+ #The file contains an array of hashes with the messages
44
+ #read it and store it as ruby code
45
+ session = Utils::Encryption.decrypt(content, config).force_encoding("UTF-8")
46
+ session_new_format = []
47
+ if session.to_s.match?(/^Me>/) #old format to be backwards compatible
48
+ session = session.split("\n")
49
+ new_value = ""
50
+ type = ""
51
+ session.each do |s|
52
+ s.gsub!('"', '\"')
53
+ if s.match?(/^(Me|chatGPT)> /)
54
+ if new_value != ""
55
+ #escape new_value to be able to store it as json
56
+ session_new_format << "{\"role\": \"#{type}\", \"content\": [{\"type\": \"text\", \"text\": #{new_value.to_json}}]}"
57
+ new_value = ""
58
+ end
59
+ if s.match?(/^Me> /)
60
+ type = "user"
61
+ else
62
+ type = "assistant"
63
+ end
64
+ s.gsub!(/^(Me|chatGPT)> /, "")
65
+ #s.gsub!("'", "\\'")
66
+ new_value += s + "\n"
67
+ else
68
+ #s.gsub!("'", "\\'")
69
+ new_value += s + "\n"
70
+ end
71
+ end
72
+ session_new_format << "{\"role\": \"#{type}\", \"content\": [{\"type\": \"text\", \"text\": #{new_value.to_json}}]}"
73
+ end
74
+ if session_new_format.empty?
75
+ #each line is json, so we need to convert it to ruby using json parser
76
+ session = session.split("\n").map{|s| JSON.parse(s, symbolize_names: true)}
77
+ else
78
+ session = []
79
+ session_new_format.each do |s|
80
+ session << JSON.parse(s, symbolize_names: true)
81
+ end
82
+ end
83
+ @ai_gpt[team_id_user][session_name] = session.deep_copy
44
84
  end
45
85
  end
46
86
  end
@@ -24,5 +24,13 @@ class SlackSmartBot
24
24
  @datetime_teams_file[file] = File.mtime(file)
25
25
  end
26
26
  end
27
+ #remove from @teams and @datetime_teams_file the keys that are not in the files
28
+ #this is to avoid having old data in memory
29
+ @teams.keys.each do |key|
30
+ unless files.include?(File.join(config.path, "teams", "t_#{key}.yaml"))
31
+ @teams.delete(key)
32
+ @datetime_teams_file.delete(File.join(config.path, "teams", "t_#{key}.yaml"))
33
+ end
34
+ end
27
35
  end
28
- end
36
+ end
@@ -1,9 +1,17 @@
1
1
  class SlackSmartBot
2
2
  def save_stats(method, data: {}, forced: false)
3
- if has_access?(method, Thread.current[:user]) or forced
3
+ if Thread.current[:user].nil? and !data[:user].to_s.empty?
4
+ user_stats = data[:user]
5
+ else
6
+ user_stats = Thread.current[:user]
7
+ end
8
+ if has_access?(method, user_stats) or forced
4
9
  if config.stats
5
10
  begin
6
- command_ids_not_to_log = ['add_vacation', 'remove_vacation', 'add_memo_team', 'set_personal_settings']
11
+ command_ids_not_to_log = [
12
+ "add_vacation", "remove_vacation", "add_memo_team", "set_personal_settings", "open_ai_chat",
13
+ "open_ai_chat_add_authorization", "open_ai_chat_copy_session_from_user",
14
+ ]
7
15
  Thread.current[:command_id] = method.to_s
8
16
  require "csv"
9
17
  if !File.exist?("#{config.stats_path}.#{Time.now.strftime("%Y-%m")}.log")
@@ -38,9 +46,9 @@ class SlackSmartBot
38
46
  end
39
47
  user_info = find_user(data.user.id)
40
48
  if user_info.nil? or user_info.is_app_user or user_info.is_bot
41
- time_zone = ''
42
- job_title = ''
43
- team_id = ''
49
+ time_zone = ""
50
+ job_title = ""
51
+ team_id = ""
44
52
  else
45
53
  time_zone = user_info.tz_label
46
54
  job_title = user_info.profile.title
@@ -0,0 +1,36 @@
1
+ class SlackSmartBot
2
+ def transform_to_slack_markdown(markdown_text)
3
+ # Regular expressions to match code blocks
4
+ code_block_regex = /```(.*?)```/m
5
+
6
+ # Extract code blocks to preserve them
7
+ preserved_code_blocks = markdown_text.scan(code_block_regex).map(&:first)
8
+
9
+ # Replace code blocks and inline code with placeholders
10
+ code_block_placeholder_text = ""
11
+ while markdown_text.include?(code_block_placeholder_text)
12
+ code_block_placeholder_text = "CODE_BLOCK_PLACEHOLDER_#{"6:x".gen}"
13
+ end
14
+ transformed_text = markdown_text.gsub(code_block_regex, code_block_placeholder_text)
15
+
16
+ # Transform general Markdown to Slack Markdown
17
+ transformed_text.gsub!(/^\* (.*)$/, '• \1') # Unordered list
18
+ transformed_text.gsub!(/^\s*d+. (.*)$/, '\1.') # Ordered list
19
+ transformed_text.gsub!(/!\[(.*?)\]\((.*?)\)/, '\1') # Images to alt text
20
+ transformed_text.gsub!(/\[(.*?)\]\((.*?)\)/, '<\2|\1>') # Links
21
+ transformed_text.gsub!(/\*\*(.*?)\*\*/, '*\1*') # Bold
22
+ transformed_text.gsub!(/__(.*?)__/, '*\1*') # Bold
23
+ # delete any * character in any position if it is a header
24
+ transformed_text.gsub!(/^\s*#+.*\*/) { |match| match.gsub("*", "") }
25
+ # add for more than 4 # the same than for #### but one :black_small_square: per extra #
26
+ transformed_text.gsub!(/^\s*####(#+) (.*)$/) { "\n" + "#{":black_small_square:" * ($1.length + 1)} *#{$2}*" }
27
+ transformed_text.gsub!(/^\s*#### (.*)$/, "\n" + ':black_small_square: *\1*') # Header level 4 to bold
28
+ transformed_text.gsub!(/^\s*### (.*)$/, "\n" + ':small_orange_diamond: *\1*') # Header level 3 to bold
29
+ transformed_text.gsub!(/^\s*## (.*)$/, "\n" + ':small_blue_diamond: *\1*') # Header level 2 to bold
30
+ transformed_text.gsub!(/^\s*# (.*)$/, "\n" + ':small_red_triangle: *\1*') # Header level 1 to bold
31
+
32
+ # Reinsert preserved code blocks and inline code
33
+ preserved_code_blocks.each { |block| transformed_text.sub!(code_block_placeholder_text, "```#{block}```") }
34
+ return transformed_text
35
+ end
36
+ end