sd-ruby-libs 0.0.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +3 -0
- data/Gemfile +3 -0
- data/excel_reports.rb +57 -0
- data/google_drive.rb +73 -0
- data/google_sheets.rb +398 -0
- data/number_format.rb +3 -0
- data/sd-ruby-libs.gemspec +20 -0
- data/slack-bot.rb +181 -0
- metadata +164 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 5585ce25566d5dfdf7447411490f187c5e1292e0
|
|
4
|
+
data.tar.gz: 499df0a567a0afc06d66c40bfac727acc584a41a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a73fce6fbdc1496e6752b1cd312eff00056e5b323476897ba79c004e0bf4032acb430b092a981f2c7987f2792a3ba05df45b7a1fd77ab69389e0205048e8fa41
|
|
7
|
+
data.tar.gz: d55c92da3739ffa84900a4b80846fd94329bd495b461bc27b742e2dd97d791ded9e7ac96a0bdf3a17502de3c53f51c8256ef2c89693a4bf045d1ea07c870d41b
|
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/excel_reports.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'writeexcel'
|
|
2
|
+
require 'write_xlsx'
|
|
3
|
+
|
|
4
|
+
# `Generic` 2d Data set to excel file generator
|
|
5
|
+
module ExcelReports
|
|
6
|
+
Youtube_time_link = Struct.new(:video, :time) do
|
|
7
|
+
def time_in_seconds
|
|
8
|
+
return 0 unless time
|
|
9
|
+
segments = time.split(':')
|
|
10
|
+
(segments[0].to_i * 3600) + (segments[1].to_i * 60) + segments[2].to_i
|
|
11
|
+
rescue
|
|
12
|
+
return 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def excel_hyperlink
|
|
16
|
+
return '' unless time
|
|
17
|
+
"=hyperlink(\"https://youtu.be/#{video}?t=#{time_in_seconds}\", \"#{time}\")"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
time
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def multi_array_to_xlsx(column_info, data, filename, freeze_panes: nil)
|
|
26
|
+
workbook ||= WriteXLSX.new("#{filename}.xlsx")
|
|
27
|
+
|
|
28
|
+
format_heading = workbook.add_format # Add a format
|
|
29
|
+
format_heading.set_bold
|
|
30
|
+
format_heading.set_align('center')
|
|
31
|
+
|
|
32
|
+
data.each do |sheet|
|
|
33
|
+
worksheet = workbook.add_worksheet sheet.first
|
|
34
|
+
|
|
35
|
+
worksheet.freeze_panes(freeze_panes[:y], freeze_panes[:x]) if freeze_panes
|
|
36
|
+
|
|
37
|
+
row = 0
|
|
38
|
+
|
|
39
|
+
column_info.each_with_index do |column, index|
|
|
40
|
+
worksheet.set_column(index, index, column[1])
|
|
41
|
+
worksheet.write(row, index, column.first, format_heading)
|
|
42
|
+
end
|
|
43
|
+
row = row.next
|
|
44
|
+
|
|
45
|
+
next unless sheet.last
|
|
46
|
+
sheet.last.each do |record|
|
|
47
|
+
record.each_with_index do |cell, index|
|
|
48
|
+
worksheet.write(row, index, cell) unless cell.last
|
|
49
|
+
worksheet.write(row, index, cell.last) if cell.last
|
|
50
|
+
end
|
|
51
|
+
row = row.next
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
workbook.close
|
|
56
|
+
end
|
|
57
|
+
end
|
data/google_drive.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'google/apis/drive_v3'
|
|
2
|
+
require 'googleauth'
|
|
3
|
+
require 'googleauth/stores/file_token_store'
|
|
4
|
+
require 'pry'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
class GoogleDriveHelper
|
|
8
|
+
def initialize
|
|
9
|
+
@GDRIVE_OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
|
10
|
+
@GDRIVE_APPLICATION_NAME = 'Drive API Ruby Quickstart'
|
|
11
|
+
@GDRIVE_CLIENT_SECRETS_PATH = 'drive_client_secret.json'
|
|
12
|
+
@GDRIVE_CREDENTIALS_PATH = File.join(Dir.home, '.credentials',
|
|
13
|
+
"drive-ruby-permissions.yaml")
|
|
14
|
+
@GDRIVE_SCOPE = Google::Apis::DriveV3::AUTH_DRIVE
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Ensure valid credentials, either by restoring from the saved credentials
|
|
19
|
+
# files or intitiating an OAuth2 authorization. If authorization is required,
|
|
20
|
+
# the user's default browser will be launched to approve the request.
|
|
21
|
+
#
|
|
22
|
+
# @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
|
|
23
|
+
def authorize
|
|
24
|
+
FileUtils.mkdir_p(File.dirname(@GDRIVE_CREDENTIALS_PATH))
|
|
25
|
+
|
|
26
|
+
client_id = Google::Auth::ClientId.from_file(@GDRIVE_CLIENT_SECRETS_PATH)
|
|
27
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: @GDRIVE_CREDENTIALS_PATH)
|
|
28
|
+
authorizer = Google::Auth::UserAuthorizer.new(
|
|
29
|
+
client_id, @GDRIVE_SCOPE, token_store)
|
|
30
|
+
user_id = 'default'
|
|
31
|
+
credentials = authorizer.get_credentials(user_id)
|
|
32
|
+
if credentials.nil?
|
|
33
|
+
url = authorizer.get_authorization_url(
|
|
34
|
+
base_url: @GDRIVE_OOB_URI)
|
|
35
|
+
puts "Open the following URL in the browser and enter the " +
|
|
36
|
+
"resulting code after authorization"
|
|
37
|
+
puts url
|
|
38
|
+
code = gets
|
|
39
|
+
credentials = authorizer.get_and_store_credentials_from_code(
|
|
40
|
+
user_id: user_id, code: code, base_url: @GDRIVE_OOB_URI)
|
|
41
|
+
end
|
|
42
|
+
credentials
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def share_document(document, email_address)
|
|
46
|
+
# Initialize the API
|
|
47
|
+
service = Google::Apis::DriveV3::DriveService.new
|
|
48
|
+
service.client_options.application_name = @GDRIVE_APPLICATION_NAME
|
|
49
|
+
service.authorization = authorize
|
|
50
|
+
|
|
51
|
+
file_id = document
|
|
52
|
+
callback = lambda do |res, err|
|
|
53
|
+
if err
|
|
54
|
+
# Handle error...
|
|
55
|
+
puts errb.psqlody
|
|
56
|
+
else
|
|
57
|
+
# puts "Permission ID: #{res.id}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
service.batch do |drive|
|
|
62
|
+
user_permission = {
|
|
63
|
+
type: 'user',
|
|
64
|
+
role: 'writer',
|
|
65
|
+
email_address: email_address
|
|
66
|
+
}
|
|
67
|
+
drive.create_permission(file_id,
|
|
68
|
+
user_permission,
|
|
69
|
+
fields: 'id',
|
|
70
|
+
&callback)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/google_sheets.rb
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
require 'google/apis/sheets_v4'
|
|
2
|
+
require 'googleauth'
|
|
3
|
+
require 'googleauth/stores/file_token_store'
|
|
4
|
+
require '../rubyLibs/google_drive.rb'
|
|
5
|
+
require 'pry'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
|
|
9
|
+
APPLICATION_NAME = 'Google Sheets API Export Tool'.freeze
|
|
10
|
+
CLIENT_SECRETS_PATH = 'client_secret.json'.freeze
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
CREDENTIALS_PATH = File.join(Dir.home, '.credentials',
|
|
14
|
+
'sheets.googleapis.com-ruby-quickstart.yaml')
|
|
15
|
+
rescue StandardError
|
|
16
|
+
# @todo had to catch for error when home isn't 'set'. need to find better place for creds to go
|
|
17
|
+
CREDENTIALS_PATH = File.join('/root/.credentials',
|
|
18
|
+
'sheets.googleapis.com-ruby-quickstart.yaml')
|
|
19
|
+
end
|
|
20
|
+
# SCOPE = Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY
|
|
21
|
+
SCOPE = Google::Apis::SheetsV4::AUTH_SPREADSHEETS
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Ensure valid credentials, either by restoring from the saved credentials
|
|
25
|
+
# files or intitiating an OAuth2 authorization. If authorization is required,
|
|
26
|
+
# the user's default browser will be launched to approve the request.
|
|
27
|
+
#
|
|
28
|
+
# @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
|
|
29
|
+
def authorize
|
|
30
|
+
FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))
|
|
31
|
+
|
|
32
|
+
client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
|
|
33
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
|
|
34
|
+
authorizer = Google::Auth::UserAuthorizer.new(
|
|
35
|
+
client_id, SCOPE, token_store
|
|
36
|
+
)
|
|
37
|
+
user_id = 'default'
|
|
38
|
+
credentials = authorizer.get_credentials(user_id)
|
|
39
|
+
if credentials.nil?
|
|
40
|
+
url = authorizer.get_authorization_url(
|
|
41
|
+
base_url: OOB_URI
|
|
42
|
+
)
|
|
43
|
+
puts 'Open the following URL in the browser and enter the ' \
|
|
44
|
+
'resulting code after authorization'
|
|
45
|
+
puts url
|
|
46
|
+
code = gets
|
|
47
|
+
credentials = authorizer.get_and_store_credentials_from_code(
|
|
48
|
+
user_id: user_id, code: code, base_url: OOB_URI
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
credentials
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Utility class to export data into specified sheet id, overwrites existing sheets completely
|
|
55
|
+
class GoogleSheetsExporter
|
|
56
|
+
attr_accessor :headers, :spreadsheet_id, :data, :sheets, :service, :email_address, :document_name
|
|
57
|
+
|
|
58
|
+
def initialize(headers, spreadsheet_id, data, email_address = nil, document_name = nil)
|
|
59
|
+
@headers = headers
|
|
60
|
+
|
|
61
|
+
@email_address = email_address
|
|
62
|
+
@document_name = document_name
|
|
63
|
+
|
|
64
|
+
@data = data
|
|
65
|
+
|
|
66
|
+
# initialize api
|
|
67
|
+
@sheets = Google::Apis::SheetsV4
|
|
68
|
+
@service = Google::Apis::SheetsV4::SheetsService.new
|
|
69
|
+
@service.client_options.application_name = APPLICATION_NAME
|
|
70
|
+
@service.authorization = authorize
|
|
71
|
+
|
|
72
|
+
return unless spreadsheet_id
|
|
73
|
+
@spreadsheet_id = spreadsheet_id unless spreadsheet_id && spreadsheet_id['https://']
|
|
74
|
+
begin
|
|
75
|
+
@spreadsheet_id = spreadsheet_id.match(%r{/([^/]{44})[/]?})[1] if spreadsheet_id['https://']
|
|
76
|
+
rescue
|
|
77
|
+
puts "Provided spreadsheet address does not work."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def spreadsheet_url
|
|
82
|
+
"https://docs.google.com/spreadsheets/d/#{spreadsheet_id}/"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def sheet_id_from_name(sheet_name)
|
|
86
|
+
retries ||= 0
|
|
87
|
+
service
|
|
88
|
+
.get_spreadsheet(spreadsheet_id)
|
|
89
|
+
.to_h[:sheets]
|
|
90
|
+
.select { |sheet| sheet[:properties][:title] == sheet_name }
|
|
91
|
+
.first[:properties][:sheet_id]
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
puts "Sheet name #{sheet_name} not found. Retry #{retries}"
|
|
94
|
+
sleep 5 ** (retries + 1) / 10.to_f
|
|
95
|
+
retry if (retries += 1) < 6
|
|
96
|
+
return 0
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def batch_update(request)
|
|
100
|
+
batch = sheets::BatchUpdateSpreadsheetRequest.new(request)
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
retries ||= 0
|
|
104
|
+
service.batch_update_spreadsheet spreadsheet_id, batch, {}
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
binding.pry if ENV['HACKING']
|
|
107
|
+
return true if e.to_s['already exists']
|
|
108
|
+
return false if e.to_s['Requested entity was not found.']
|
|
109
|
+
puts "Error with google API calling #{request} on retry #{retries}"
|
|
110
|
+
puts e
|
|
111
|
+
sleep 5 ** (retries + 1) / 10.to_f
|
|
112
|
+
retry if (retries += 1) < 6 and not e.to_s['Invalid request']
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def auto_resize_sheet(sheet)
|
|
117
|
+
batch_update(requests: [
|
|
118
|
+
{
|
|
119
|
+
auto_resize_dimensions: {
|
|
120
|
+
dimensions: {
|
|
121
|
+
sheet_id: sheet_id_from_name(sheet.first),
|
|
122
|
+
dimension: 'COLUMNS'
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def sheet_default_font(sheet)
|
|
130
|
+
batch_update(requests: [
|
|
131
|
+
{
|
|
132
|
+
repeat_cell: {
|
|
133
|
+
range: {
|
|
134
|
+
sheet_id: sheet_id_from_name(sheet.first)
|
|
135
|
+
},
|
|
136
|
+
cell: {
|
|
137
|
+
user_entered_format: {
|
|
138
|
+
text_format: {
|
|
139
|
+
font_size: 11,
|
|
140
|
+
font_family: 'Arial'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
fields: 'user_entered_format(text_format, horizontal_alignment)'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def bold_row(sheet, start_row_index, end_row_index)
|
|
151
|
+
batch_update(requests: [
|
|
152
|
+
{
|
|
153
|
+
repeat_cell: {
|
|
154
|
+
range: {
|
|
155
|
+
sheet_id: sheet_id_from_name(sheet.first),
|
|
156
|
+
start_row_index: start_row_index,
|
|
157
|
+
end_row_index: end_row_index + 1
|
|
158
|
+
},
|
|
159
|
+
cell: {
|
|
160
|
+
user_entered_format: {
|
|
161
|
+
horizontal_alignment: 'CENTER',
|
|
162
|
+
text_format: {
|
|
163
|
+
foreground_color: {
|
|
164
|
+
red: 20.0 / 255,
|
|
165
|
+
green: 108.0 / 255,
|
|
166
|
+
blue: 124.0 / 255
|
|
167
|
+
},
|
|
168
|
+
font_size: 11,
|
|
169
|
+
bold: true
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
fields: 'user_entered_format(text_format, horizontal_alignment)'
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
update_sheet_properties: {
|
|
178
|
+
properties: {
|
|
179
|
+
sheet_id: sheet_id_from_name(sheet.first),
|
|
180
|
+
grid_properties: {
|
|
181
|
+
frozen_row_count: 1
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
fields: 'grid_properties.frozen_row_count'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format_column(sheet, start_column_index, end_column_index, horizontal_alignment = 'left', bold = false, font_size = 11)
|
|
191
|
+
horizontal_alignment = 'left' unless horizontal_alignment
|
|
192
|
+
batch_update(requests: [
|
|
193
|
+
{
|
|
194
|
+
repeat_cell: {
|
|
195
|
+
range: {
|
|
196
|
+
sheet_id: sheet_id_from_name(sheet.first),
|
|
197
|
+
start_column_index: start_column_index,
|
|
198
|
+
end_column_index: end_column_index + 1
|
|
199
|
+
},
|
|
200
|
+
cell: {
|
|
201
|
+
user_entered_format: {
|
|
202
|
+
horizontal_alignment: horizontal_alignment.upcase,
|
|
203
|
+
text_format: {
|
|
204
|
+
font_size: font_size,
|
|
205
|
+
bold: bold
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
fields: 'user_entered_format(text_format, horizontal_alignment)'
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
])
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def number_format_column(sheet, start_column_index, end_column_index, pattern = '#,##0.00')
|
|
216
|
+
batch_update(requests: [
|
|
217
|
+
{
|
|
218
|
+
repeat_cell: {
|
|
219
|
+
range: {
|
|
220
|
+
sheet_id: sheet_id_from_name(sheet.first),
|
|
221
|
+
start_column_index: start_column_index,
|
|
222
|
+
end_column_index: end_column_index + 1
|
|
223
|
+
},
|
|
224
|
+
cell: {
|
|
225
|
+
user_entered_format: {
|
|
226
|
+
number_format: {
|
|
227
|
+
type: 'NUMBER',
|
|
228
|
+
pattern: pattern
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
fields: 'user_entered_format(number_format)'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
])
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def create_temp_sheet
|
|
239
|
+
batch_update(requests: [{
|
|
240
|
+
add_sheet: {
|
|
241
|
+
properties: {
|
|
242
|
+
title: 'temp-sheet',
|
|
243
|
+
grid_properties: {
|
|
244
|
+
column_count: 1
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}])
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def delete_all_sheets
|
|
252
|
+
service
|
|
253
|
+
.get_spreadsheet(spreadsheet_id)
|
|
254
|
+
.to_h[:sheets]
|
|
255
|
+
.select { |sheet| sheet[:properties][:title] != 'temp-sheet' }
|
|
256
|
+
.each do |sheet|
|
|
257
|
+
batch_update(requests: [{
|
|
258
|
+
delete_sheet: {
|
|
259
|
+
sheet_id: sheet[:properties][:sheet_id]
|
|
260
|
+
}
|
|
261
|
+
}])
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def column_from_index(index)
|
|
266
|
+
%w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)[index - 1]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def populate_new_sheets
|
|
270
|
+
threads = []
|
|
271
|
+
data.each do |sheet|
|
|
272
|
+
# threads.push Thread.new {
|
|
273
|
+
|
|
274
|
+
begin
|
|
275
|
+
batch_update(requests: [{
|
|
276
|
+
add_sheet: {
|
|
277
|
+
properties: {
|
|
278
|
+
title: sheet.first,
|
|
279
|
+
grid_properties: {
|
|
280
|
+
column_count: sheet.last.first.count
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}])
|
|
285
|
+
|
|
286
|
+
rescue
|
|
287
|
+
binding.pry if ENV['HACKING']
|
|
288
|
+
puts sheet
|
|
289
|
+
puts "invalid or no data, skipping"
|
|
290
|
+
next
|
|
291
|
+
end
|
|
292
|
+
sheet_default_font(sheet)
|
|
293
|
+
|
|
294
|
+
headers.each_with_index do |header, index|
|
|
295
|
+
format_column sheet, index, index, header[1]
|
|
296
|
+
number_format_column sheet, index, index, header[2] if header[2]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
value_range_object = {
|
|
300
|
+
major_dimension: 'ROWS',
|
|
301
|
+
values: [headers.map(&:first)]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
range = sheet.first
|
|
305
|
+
|
|
306
|
+
begin
|
|
307
|
+
retries ||= 0
|
|
308
|
+
service.update_spreadsheet_value(spreadsheet_id, range, value_range_object, value_input_option: 'USER_ENTERED')
|
|
309
|
+
rescue StandardError => e
|
|
310
|
+
puts "Failed to save data, retry #{retries}"
|
|
311
|
+
puts e
|
|
312
|
+
sleep 5 ** (retries + 1) / 10.to_f
|
|
313
|
+
retry if (retries += 1) < 6
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
bold_row(sheet, 0, 0)
|
|
317
|
+
|
|
318
|
+
if sheet.last.first.respond_to?('values')
|
|
319
|
+
value_range_object = {
|
|
320
|
+
major_dimension: 'ROWS',
|
|
321
|
+
values: sheet.last.map(&:values)
|
|
322
|
+
}
|
|
323
|
+
else
|
|
324
|
+
value_range_object = {
|
|
325
|
+
major_dimension: 'ROWS',
|
|
326
|
+
values: sheet.last.map(&:itself)
|
|
327
|
+
}
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
range = "#{sheet.first}!A2:#{column_from_index headers.count}#{sheet.last.count + 1}"
|
|
331
|
+
|
|
332
|
+
begin
|
|
333
|
+
retries ||= 0
|
|
334
|
+
|
|
335
|
+
service.update_spreadsheet_value(spreadsheet_id, range, value_range_object, value_input_option: 'USER_ENTERED')
|
|
336
|
+
rescue StandardError => e
|
|
337
|
+
puts "Failed to save data, retry #{retries}"
|
|
338
|
+
puts e
|
|
339
|
+
sleep 5 ** (retries + 1) / 10.to_f
|
|
340
|
+
retry if (retries += 1) < 6
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
auto_resize_sheet(sheet)
|
|
344
|
+
# }
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
threads.each(&:join)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def delete_temp_sheet
|
|
351
|
+
service
|
|
352
|
+
.get_spreadsheet(spreadsheet_id)
|
|
353
|
+
.to_h[:sheets]
|
|
354
|
+
.select { |sheet| sheet[:properties][:title] == 'temp-sheet' }
|
|
355
|
+
.each do |sheet|
|
|
356
|
+
batch_update(requests: [{
|
|
357
|
+
delete_sheet: {
|
|
358
|
+
sheet_id: sheet[:properties][:sheet_id]
|
|
359
|
+
}
|
|
360
|
+
}])
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def update_google_sheet
|
|
365
|
+
return puts "Invalid Google Sheet. Please make sure it is shared with sdegabriele@bbtv.com" unless create_temp_sheet
|
|
366
|
+
delete_all_sheets
|
|
367
|
+
populate_new_sheets
|
|
368
|
+
delete_temp_sheet
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def create_sheet
|
|
372
|
+
request_body = Google::Apis::SheetsV4::Spreadsheet.new
|
|
373
|
+
response = service.create_spreadsheet(request_body)
|
|
374
|
+
@spreadsheet_id = response.spreadsheet_id
|
|
375
|
+
|
|
376
|
+
requests = []
|
|
377
|
+
requests.push(
|
|
378
|
+
update_spreadsheet_properties: {
|
|
379
|
+
properties: { title: document_name },
|
|
380
|
+
fields: 'title'
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
batch_update(requests: requests)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def share_sheet
|
|
387
|
+
GoogleDriveHelper.new.share_document(spreadsheet_id, email_address)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def create_google_sheet
|
|
391
|
+
create_sheet
|
|
392
|
+
share_sheet
|
|
393
|
+
create_temp_sheet
|
|
394
|
+
delete_all_sheets
|
|
395
|
+
populate_new_sheets
|
|
396
|
+
delete_temp_sheet
|
|
397
|
+
end
|
|
398
|
+
end
|
data/number_format.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Gem::Specification.new do |gem|
|
|
2
|
+
gem.name = 'sd-ruby-libs'
|
|
3
|
+
gem.version = `git describe --tags --abbrev=0`.chomp
|
|
4
|
+
gem.summary = "various tools"
|
|
5
|
+
gem.description = "basic slack and google sheets functionality"
|
|
6
|
+
gem.authors = ["Stephen DeGabriele"]
|
|
7
|
+
gem.email = 'someguy@degabriele.com'
|
|
8
|
+
|
|
9
|
+
gem.files = `git ls-files`.split("\n")
|
|
10
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
12
|
+
gem.require_paths = ['lib']
|
|
13
|
+
|
|
14
|
+
gem.add_runtime_dependency 'google-api-client', '~> 0.13.4', '>= 0.13.4'
|
|
15
|
+
gem.add_runtime_dependency 'googleauth', '~> 0.5.3', '>= 0.5.3'
|
|
16
|
+
gem.add_runtime_dependency 'slack-api', '~> 1.6.0', '>= 1.6.0'
|
|
17
|
+
gem.add_runtime_dependency 'write_xlsx', '~> 0.85.1', '>= 0.85.1'
|
|
18
|
+
gem.add_runtime_dependency 'writeexcel', '~> 1.0.5', '>= 1.0.5'
|
|
19
|
+
gem.add_runtime_dependency 'pry'
|
|
20
|
+
end
|
data/slack-bot.rb
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'slack'
|
|
3
|
+
require 'kanban'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'redis'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'pry'
|
|
8
|
+
|
|
9
|
+
# Simple wrapper for the slack ruby interface
|
|
10
|
+
module SlackNotification
|
|
11
|
+
def initialize
|
|
12
|
+
@restarts = 0
|
|
13
|
+
@config = YAML.load_file(ENV['SLACKCONFIG'] || 'slackConfig.yaml')
|
|
14
|
+
|
|
15
|
+
Slack.configure do |config|
|
|
16
|
+
config.token = @config['slack_token']
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def redis
|
|
21
|
+
@redis ||= Redis.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def worker_backlog
|
|
25
|
+
@worker_backlog ||= Kanban::Backlog.new backend: Redis.new(db: 3), namespace: "#{name}-worker"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def dispatch_backlog
|
|
29
|
+
@dispatch_backlog ||= Kanban::Backlog.new backend: Redis.new(db: 3), namespace: "#{name}-dispatch"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def client
|
|
33
|
+
@client ||= Slack::Client.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_channel
|
|
37
|
+
@config['slack_channels'].each do |channel|
|
|
38
|
+
client.groups_create(name: channel)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def invite(who)
|
|
43
|
+
@config['slack_channels'].each do |channel|
|
|
44
|
+
client.groups_invite(channel: channel, user: who)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_interface(*message)
|
|
49
|
+
create_channel
|
|
50
|
+
invite message.first['user']
|
|
51
|
+
help
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def name
|
|
55
|
+
@config['slack_name']
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def help(message)
|
|
59
|
+
send_notification(known_commands.to_s, message['channel'])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_command(message)
|
|
63
|
+
unsure = [
|
|
64
|
+
"I heard my name.... instructions unclear. `#{name} help` might be what you need. :dog:",
|
|
65
|
+
":dog: Sometimes when you say my name I think you are going to give me a treat. `#{name} help` :dog:",
|
|
66
|
+
":dog: wag :dog: `#{name} help` might be what you want.",
|
|
67
|
+
"woof. Not sure what you mean. Please try \"#{name} help\"",
|
|
68
|
+
":dog: feeling confused? Me too. Might be a good time for a sandwich! then try \"#{name} help\"",
|
|
69
|
+
"http://puppyloveblog.com/scaled/600x450/taro-the-black-labrador-puppy-and-his-sad-puppy-dog-eyes-451.jpg Try `#{name} help` for assistance.",
|
|
70
|
+
"http://puppyloveblog.com/scaled/600x450/taro-the-black-labrador-puppy-being-cute-519.jpg `#{name} help` to see what i can do.",
|
|
71
|
+
"http://puppyloveblog.com/scaled/600x337/kratos-and-his-rope-what-a-handsome-black-labrador-retriever-puppy-471.jpg `#{name} help` will pull up the list of commands i know"
|
|
72
|
+
]
|
|
73
|
+
send_notification(unsure.sample, message['channel'])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def bot_message?(m)
|
|
77
|
+
m['subtype'] && m['subtype'] == 'bot_message'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def magic_phrase?(m)
|
|
81
|
+
return unless m['text'] && m['text'].start_with?("#{name} juice #{name} juice #{name} juice")
|
|
82
|
+
send_notification "It's showtime #{m}", m['channel']
|
|
83
|
+
# need to have auth users before using this
|
|
84
|
+
# create_interface m
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def configured_channel?(m)
|
|
89
|
+
return false if @config['slack_blacklist_channels'] && @config['slack_blacklist_channels'].include?(m['channel'])
|
|
90
|
+
@config['slack_channels'].include? m['channel'] or @config['any_channel']
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def message_for_bot?(m)
|
|
94
|
+
m['text'] && m['text'].start_with?(name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def valid_command(command_matrix, m)
|
|
98
|
+
commands = command_matrix.select do |keyword|
|
|
99
|
+
m['text'].start_with? "#{name} #{keyword.first}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
[commands.sort_by { |command| command.first.length }.reverse.first]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def skip_message?(m)
|
|
106
|
+
return true if bot_message? m
|
|
107
|
+
return true if magic_phrase? m
|
|
108
|
+
return true unless configured_channel? m
|
|
109
|
+
return true unless message_for_bot? m
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def queue_command(command, message)
|
|
113
|
+
message['command'] = command
|
|
114
|
+
job = { 'task' => JSON.dump(message) }
|
|
115
|
+
worker_backlog.add job
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def perform_command(matrix, m)
|
|
119
|
+
action = valid_command(matrix, m) - [nil]
|
|
120
|
+
return send_notification("Command not known. Please try help.", m['channel']) unless action
|
|
121
|
+
return send('parse_command', m) if action.count.zero?
|
|
122
|
+
return send_notification("It is not clear what you want. Valid matching actions are: #{action}", m['channel']) if action.count > 1
|
|
123
|
+
queue_command(action.first.last, m)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def worker
|
|
127
|
+
loop do
|
|
128
|
+
task_id = worker_backlog.claim(duration: 999999)
|
|
129
|
+
message = JSON.parse(worker_backlog.get(task_id)['task'])
|
|
130
|
+
command = message['command']
|
|
131
|
+
puts "Debug info: command = #{command} message = #{message}"
|
|
132
|
+
send(command, message)
|
|
133
|
+
worker_backlog.complete task_id
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def real_time(matrix, restarted = false)
|
|
138
|
+
rtm = client.realtime
|
|
139
|
+
message_count = 0
|
|
140
|
+
rtm.on :message do |m|
|
|
141
|
+
puts m if ENV['DEBUGGING']
|
|
142
|
+
message_count += 1
|
|
143
|
+
next if restarted && message_count == 1
|
|
144
|
+
next if skip_message? m
|
|
145
|
+
perform_command matrix, m
|
|
146
|
+
end
|
|
147
|
+
rtm.start
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def send_binary_file(file, override_filename = nil)
|
|
151
|
+
puts "Sending binary file: #{file}"
|
|
152
|
+
override_filename = file.split('/').last unless override_filename
|
|
153
|
+
`curl -F file=@#{file} -F channels=#{@config['slack_channels'].first} -F token=#{@config['slack_token']} -F filename=#{override_filename} https://slack.com/api/files.upload 2>&1`
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def queue_notification(message, channel = nil)
|
|
157
|
+
task = {
|
|
158
|
+
message: message,
|
|
159
|
+
channel: channel ? channel : @config['slack_channels'].first
|
|
160
|
+
}
|
|
161
|
+
job = { 'task' => JSON.dump(task) }
|
|
162
|
+
dispatch_backlog.add job
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def transmission
|
|
166
|
+
loop do
|
|
167
|
+
task_id = dispatch_backlog.claim(duration: 999999)
|
|
168
|
+
job = JSON.parse(dispatch_backlog.get(task_id)['task'])
|
|
169
|
+
next unless job['message']
|
|
170
|
+
puts "Debug info: channel = #{job['channel']} message = #{job['message']}"
|
|
171
|
+
send_notification job['message'], job['channel']
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def send_notification(message, channel = nil)
|
|
176
|
+
return queue_notification message, channel unless ENV['TRANSMISSION']
|
|
177
|
+
puts "Sending notification: #{message}"
|
|
178
|
+
channel ||= @config['slack_channels'].first
|
|
179
|
+
client.chat_postMessage(channel: channel, text: message, as_user: false, username: @config['slack_name'])
|
|
180
|
+
end
|
|
181
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sd-ruby-libs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stephen DeGabriele
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-02-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: google-api-client
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.13.4
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 0.13.4
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 0.13.4
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.13.4
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: googleauth
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.5.3
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 0.5.3
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - "~>"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 0.5.3
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 0.5.3
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: slack-api
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 1.6.0
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 1.6.0
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - "~>"
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 1.6.0
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 1.6.0
|
|
73
|
+
- !ruby/object:Gem::Dependency
|
|
74
|
+
name: write_xlsx
|
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 0.85.1
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 0.85.1
|
|
83
|
+
type: :runtime
|
|
84
|
+
prerelease: false
|
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 0.85.1
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: 0.85.1
|
|
93
|
+
- !ruby/object:Gem::Dependency
|
|
94
|
+
name: writeexcel
|
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - "~>"
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: 1.0.5
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.0.5
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 1.0.5
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: 1.0.5
|
|
113
|
+
- !ruby/object:Gem::Dependency
|
|
114
|
+
name: pry
|
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
type: :runtime
|
|
121
|
+
prerelease: false
|
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '0'
|
|
127
|
+
description: basic slack and google sheets functionality
|
|
128
|
+
email: someguy@degabriele.com
|
|
129
|
+
executables: []
|
|
130
|
+
extensions: []
|
|
131
|
+
extra_rdoc_files: []
|
|
132
|
+
files:
|
|
133
|
+
- ".rubocop.yml"
|
|
134
|
+
- Gemfile
|
|
135
|
+
- excel_reports.rb
|
|
136
|
+
- google_drive.rb
|
|
137
|
+
- google_sheets.rb
|
|
138
|
+
- number_format.rb
|
|
139
|
+
- sd-ruby-libs.gemspec
|
|
140
|
+
- slack-bot.rb
|
|
141
|
+
homepage:
|
|
142
|
+
licenses: []
|
|
143
|
+
metadata: {}
|
|
144
|
+
post_install_message:
|
|
145
|
+
rdoc_options: []
|
|
146
|
+
require_paths:
|
|
147
|
+
- lib
|
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
158
|
+
requirements: []
|
|
159
|
+
rubyforge_project:
|
|
160
|
+
rubygems_version: 2.5.2.1
|
|
161
|
+
signing_key:
|
|
162
|
+
specification_version: 4
|
|
163
|
+
summary: various tools
|
|
164
|
+
test_files: []
|