slk 0.7.0 → 0.9.0
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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +71 -1
- data/lib/slk/api/users.rb +27 -0
- data/lib/slk/cli.rb +2 -1
- data/lib/slk/commands/deactivations.rb +285 -0
- data/lib/slk/commands/help.rb +25 -18
- data/lib/slk/commands/status.rb +142 -17
- data/lib/slk/formatters/deactivation_formatter.rb +108 -0
- data/lib/slk/formatters/json_status_formatter.rb +91 -0
- data/lib/slk/models/deactivation.rb +77 -0
- data/lib/slk/models/dnd_state.rb +81 -0
- data/lib/slk/models/status_snapshot.rb +30 -0
- data/lib/slk/services/deactivation_scanner.rb +105 -0
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +7 -0
- metadata +9 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Services
|
|
5
|
+
# Slack has no "who left" endpoint. What it does have is users.list, which
|
|
6
|
+
# returns deactivated accounts with `deleted: true` and an `updated` epoch
|
|
7
|
+
# for the last change to the account — and for a deactivated account that
|
|
8
|
+
# change is nearly always the deactivation itself.
|
|
9
|
+
#
|
|
10
|
+
# The full roster is one API call per 1000 members, so the derived result
|
|
11
|
+
# (deactivated members plus headcounts, not the whole roster) is cached in
|
|
12
|
+
# the workspace meta cache with a TTL.
|
|
13
|
+
class DeactivationScanner
|
|
14
|
+
CACHE_KEY = 'deactivations_v1'
|
|
15
|
+
DEFAULT_TTL = 21_600 # 6 hours
|
|
16
|
+
REQUIRED_KEYS = %w[fetched_at member_count human_count active_count records].freeze
|
|
17
|
+
|
|
18
|
+
Report = Data.define(:records, :member_count, :human_count, :active_count, :fetched_at) do
|
|
19
|
+
def deactivated_count = records.size
|
|
20
|
+
|
|
21
|
+
def bots = records.count(&:bot)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(users_api:, workspace_name:, cache_store: nil, ttl: DEFAULT_TTL, on_debug: nil)
|
|
25
|
+
@users_api = users_api
|
|
26
|
+
@workspace_name = workspace_name
|
|
27
|
+
@cache = cache_store
|
|
28
|
+
@ttl = ttl
|
|
29
|
+
@on_debug = on_debug
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Report] every deactivated account, newest deactivation first
|
|
33
|
+
def scan(refresh: false)
|
|
34
|
+
build_report(cached(refresh: refresh) || store(collect))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def cached(refresh:)
|
|
40
|
+
return nil if refresh
|
|
41
|
+
|
|
42
|
+
data = MetaCache.read(@cache, @workspace_name, CACHE_KEY, ttl: @ttl)
|
|
43
|
+
return data if usable?(data)
|
|
44
|
+
|
|
45
|
+
@on_debug&.call('deactivations cache is unusable; re-fetching the roster') if data
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A truncated or older-shaped entry would otherwise be read field by
|
|
50
|
+
# field into a report of zero active members and zero departures — a
|
|
51
|
+
# confident, wrong answer. Missing anything required means refetch.
|
|
52
|
+
def usable?(data)
|
|
53
|
+
data.is_a?(Hash) && data['records'].is_a?(Array) && REQUIRED_KEYS.all? { |key| data[key] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def store(data)
|
|
57
|
+
MetaCache.write(@cache, @workspace_name, CACHE_KEY, data)
|
|
58
|
+
data
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def collect
|
|
62
|
+
members = fetch_members
|
|
63
|
+
deleted = members.select { |m| m['deleted'] }
|
|
64
|
+
|
|
65
|
+
counts(members).merge(
|
|
66
|
+
'fetched_at' => Time.now.to_i,
|
|
67
|
+
'records' => deleted.map { |m| Models::Deactivation.from_api(m).to_cache }
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def counts(members)
|
|
72
|
+
humans = members.reject { |m| Models::Deactivation.bot?(m) }
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
'member_count' => members.size,
|
|
76
|
+
'human_count' => humans.size,
|
|
77
|
+
'active_count' => humans.count { |m| !m['deleted'] }
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def fetch_members
|
|
82
|
+
@users_api.list_all do |total|
|
|
83
|
+
@on_debug&.call("users.list: #{total} members fetched")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_report(data)
|
|
88
|
+
Report.new(
|
|
89
|
+
records: sorted_records(data['records'] || []),
|
|
90
|
+
member_count: data['member_count'].to_i,
|
|
91
|
+
human_count: data['human_count'].to_i,
|
|
92
|
+
active_count: data['active_count'].to_i,
|
|
93
|
+
fetched_at: data['fetched_at']&.to_i
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Newest first; accounts with no usable timestamp sort to the bottom
|
|
98
|
+
# rather than pretending to be from 1970.
|
|
99
|
+
def sorted_records(raw)
|
|
100
|
+
raw.map { |hash| Models::Deactivation.from_cache(hash) }
|
|
101
|
+
.sort_by { |record| -(record.deactivated_at || 0) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/slk/version.rb
CHANGED
data/lib/slk.rb
CHANGED
|
@@ -67,11 +67,14 @@ module Slk
|
|
|
67
67
|
autoload :Workspace, 'slk/models/workspace'
|
|
68
68
|
autoload :Status, 'slk/models/status'
|
|
69
69
|
autoload :ScheduledStatus, 'slk/models/scheduled_status'
|
|
70
|
+
autoload :DndState, 'slk/models/dnd_state'
|
|
71
|
+
autoload :StatusSnapshot, 'slk/models/status_snapshot'
|
|
70
72
|
autoload :Message, 'slk/models/message'
|
|
71
73
|
autoload :Reaction, 'slk/models/reaction'
|
|
72
74
|
autoload :User, 'slk/models/user'
|
|
73
75
|
autoload :Channel, 'slk/models/channel'
|
|
74
76
|
autoload :Preset, 'slk/models/preset'
|
|
77
|
+
autoload :Deactivation, 'slk/models/deactivation'
|
|
75
78
|
autoload :SearchResult, 'slk/models/search_result'
|
|
76
79
|
autoload :SavedItem, 'slk/models/saved_item'
|
|
77
80
|
autoload :Profile, 'slk/models/profile'
|
|
@@ -105,6 +108,7 @@ module Slk
|
|
|
105
108
|
autoload :ProfileBuilder, 'slk/services/profile_builder'
|
|
106
109
|
autoload :ProfileResolver, 'slk/services/profile_resolver'
|
|
107
110
|
autoload :MetaCache, 'slk/services/meta_cache'
|
|
111
|
+
autoload :DeactivationScanner, 'slk/services/deactivation_scanner'
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
# Output formatters for messages, durations, and emoji
|
|
@@ -117,6 +121,7 @@ module Slk
|
|
|
117
121
|
autoload :MessageFormatter, 'slk/formatters/message_formatter'
|
|
118
122
|
autoload :ReactionFormatter, 'slk/formatters/reaction_formatter'
|
|
119
123
|
autoload :JsonMessageFormatter, 'slk/formatters/json_message_formatter'
|
|
124
|
+
autoload :JsonStatusFormatter, 'slk/formatters/json_status_formatter'
|
|
120
125
|
autoload :ActivityFormatter, 'slk/formatters/activity_formatter'
|
|
121
126
|
autoload :AttachmentFormatter, 'slk/formatters/attachment_formatter'
|
|
122
127
|
autoload :BlockFormatter, 'slk/formatters/block_formatter'
|
|
@@ -126,6 +131,7 @@ module Slk
|
|
|
126
131
|
autoload :ProfileFormatter, 'slk/formatters/profile_formatter'
|
|
127
132
|
autoload :ProfileFieldRenderer, 'slk/formatters/profile_field_renderer'
|
|
128
133
|
autoload :ProfileRows, 'slk/formatters/profile_rows'
|
|
134
|
+
autoload :DeactivationFormatter, 'slk/formatters/deactivation_formatter'
|
|
129
135
|
end
|
|
130
136
|
|
|
131
137
|
# CLI commands implementing user-facing functionality
|
|
@@ -150,6 +156,7 @@ module Slk
|
|
|
150
156
|
autoload :Debug, 'slk/commands/debug'
|
|
151
157
|
autoload :Who, 'slk/commands/who'
|
|
152
158
|
autoload :Org, 'slk/commands/org'
|
|
159
|
+
autoload :Deactivations, 'slk/commands/deactivations'
|
|
153
160
|
end
|
|
154
161
|
|
|
155
162
|
# Thin wrappers around Slack API endpoints
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Boehs
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/slk/commands/cache.rb
|
|
46
46
|
- lib/slk/commands/catchup.rb
|
|
47
47
|
- lib/slk/commands/config.rb
|
|
48
|
+
- lib/slk/commands/deactivations.rb
|
|
48
49
|
- lib/slk/commands/debug.rb
|
|
49
50
|
- lib/slk/commands/dnd.rb
|
|
50
51
|
- lib/slk/commands/emoji.rb
|
|
@@ -64,9 +65,11 @@ files:
|
|
|
64
65
|
- lib/slk/formatters/activity_formatter.rb
|
|
65
66
|
- lib/slk/formatters/attachment_formatter.rb
|
|
66
67
|
- lib/slk/formatters/block_formatter.rb
|
|
68
|
+
- lib/slk/formatters/deactivation_formatter.rb
|
|
67
69
|
- lib/slk/formatters/duration_formatter.rb
|
|
68
70
|
- lib/slk/formatters/emoji_replacer.rb
|
|
69
71
|
- lib/slk/formatters/json_message_formatter.rb
|
|
72
|
+
- lib/slk/formatters/json_status_formatter.rb
|
|
70
73
|
- lib/slk/formatters/markdown_output.rb
|
|
71
74
|
- lib/slk/formatters/mention_replacer.rb
|
|
72
75
|
- lib/slk/formatters/message_formatter.rb
|
|
@@ -79,6 +82,8 @@ files:
|
|
|
79
82
|
- lib/slk/formatters/search_formatter.rb
|
|
80
83
|
- lib/slk/formatters/text_processor.rb
|
|
81
84
|
- lib/slk/models/channel.rb
|
|
85
|
+
- lib/slk/models/deactivation.rb
|
|
86
|
+
- lib/slk/models/dnd_state.rb
|
|
82
87
|
- lib/slk/models/duration.rb
|
|
83
88
|
- lib/slk/models/message.rb
|
|
84
89
|
- lib/slk/models/preset.rb
|
|
@@ -89,6 +94,7 @@ files:
|
|
|
89
94
|
- lib/slk/models/scheduled_status.rb
|
|
90
95
|
- lib/slk/models/search_result.rb
|
|
91
96
|
- lib/slk/models/status.rb
|
|
97
|
+
- lib/slk/models/status_snapshot.rb
|
|
92
98
|
- lib/slk/models/user.rb
|
|
93
99
|
- lib/slk/models/workspace.rb
|
|
94
100
|
- lib/slk/runner.rb
|
|
@@ -96,6 +102,7 @@ files:
|
|
|
96
102
|
- lib/slk/services/api_client.rb
|
|
97
103
|
- lib/slk/services/cache_store.rb
|
|
98
104
|
- lib/slk/services/configuration.rb
|
|
105
|
+
- lib/slk/services/deactivation_scanner.rb
|
|
99
106
|
- lib/slk/services/emoji_downloader.rb
|
|
100
107
|
- lib/slk/services/emoji_searcher.rb
|
|
101
108
|
- lib/slk/services/encryption.rb
|
|
@@ -152,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
152
159
|
- !ruby/object:Gem::Version
|
|
153
160
|
version: '0'
|
|
154
161
|
requirements: []
|
|
155
|
-
rubygems_version: 4.0.
|
|
162
|
+
rubygems_version: 4.0.16
|
|
156
163
|
specification_version: 4
|
|
157
164
|
summary: A command-line interface for Slack
|
|
158
165
|
test_files: []
|