gitlab-triage 1.53.1 → 1.55.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/Gemfile +3 -0
- data/Gemfile.lock +4 -3
- data/README.md +173 -5
- data/lib/gitlab/triage/action/base.rb +62 -0
- data/lib/gitlab/triage/action/comment.rb +1 -9
- data/lib/gitlab/triage/action/comment_on_summary.rb +0 -6
- data/lib/gitlab/triage/action/issue.rb +36 -3
- data/lib/gitlab/triage/action/summarize.rb +26 -1
- data/lib/gitlab/triage/action/update.rb +454 -0
- data/lib/gitlab/triage/action/work_item.rb +0 -27
- data/lib/gitlab/triage/action.rb +18 -2
- data/lib/gitlab/triage/command_builders/cc_command_builder.rb +0 -4
- data/lib/gitlab/triage/engine.rb +13 -4
- data/lib/gitlab/triage/entity_builders/issue_builder.rb +8 -0
- data/lib/gitlab/triage/entity_builders/issue_fields.rb +65 -0
- data/lib/gitlab/triage/entity_builders/summary_builder.rb +8 -0
- data/lib/gitlab/triage/graphql_network.rb +22 -5
- data/lib/gitlab/triage/graphql_queries/query_builder.rb +46 -3
- data/lib/gitlab/triage/network.rb +4 -0
- data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +24 -0
- data/lib/gitlab/triage/network_adapters/test_adapter.rb +8 -0
- data/lib/gitlab/triage/policies/base_policy.rb +10 -2
- data/lib/gitlab/triage/policies/rule_policy.rb +18 -8
- data/lib/gitlab/triage/policies/summary_policy.rb +9 -0
- data/lib/gitlab/triage/rest_api_network.rb +22 -0
- data/lib/gitlab/triage/validators/policy_validator.rb +1 -3
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +4 -6
- data/lib/gitlab/triage/command_builders/label_command_builder.rb +0 -42
- data/lib/gitlab/triage/command_builders/move_command_builder.rb +0 -21
- data/lib/gitlab/triage/command_builders/remove_label_command_builder.rb +0 -17
- data/lib/gitlab/triage/command_builders/status_command_builder.rb +0 -25
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../command_builders/text_content_builder'
|
|
4
|
+
require_relative 'issue_fields'
|
|
4
5
|
|
|
5
6
|
module Gitlab
|
|
6
7
|
module Triage
|
|
7
8
|
module EntityBuilders
|
|
8
9
|
class SummaryBuilder
|
|
10
|
+
include IssueFields
|
|
11
|
+
|
|
9
12
|
def initialize(
|
|
10
13
|
type:, action:, resources:, network:,
|
|
11
14
|
policy_spec: {}, separator: "\n")
|
|
@@ -21,6 +24,7 @@ module Gitlab
|
|
|
21
24
|
@network = network
|
|
22
25
|
@separator = separator
|
|
23
26
|
@is_custom = policy_spec[:type] == 'custom'
|
|
27
|
+
init_issue_fields(action)
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def title
|
|
@@ -53,6 +57,10 @@ module Gitlab
|
|
|
53
57
|
|
|
54
58
|
private
|
|
55
59
|
|
|
60
|
+
def evaluate(template)
|
|
61
|
+
build_text(title_resource, template)
|
|
62
|
+
end
|
|
63
|
+
|
|
56
64
|
def title_present?
|
|
57
65
|
/\S+/.match?(title)
|
|
58
66
|
end
|
|
@@ -71,14 +71,31 @@ module Gitlab
|
|
|
71
71
|
|
|
72
72
|
private
|
|
73
73
|
|
|
74
|
+
# Builds a resource from one GraphQL node, holding only the fields the response
|
|
75
|
+
# actually carried. The query asks for a field only when the rule's conditions
|
|
76
|
+
# or actions need it, and the caller merges this over the REST resource, so a
|
|
77
|
+
# key written for a field nobody asked for would wipe what REST returned.
|
|
74
78
|
def normalize(resource)
|
|
75
79
|
resource
|
|
76
80
|
.slice(:iid, :title, :state, :author, :merged_at, :user_notes_count, :user_discussions_count, :upvotes, :downvotes, :project_id, :web_url)
|
|
77
|
-
.merge(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
.merge(id: extract_id_from_global_id(resource[:id]))
|
|
82
|
+
.merge(normalize_labels(resource))
|
|
83
|
+
.merge(normalize_users(resource, :assignees))
|
|
84
|
+
.merge(normalize_users(resource, :reviewers))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalize_labels(resource)
|
|
88
|
+
nodes = resource.dig(:labels, :nodes)
|
|
89
|
+
return {} unless nodes
|
|
90
|
+
|
|
91
|
+
{ labels: nodes.pluck(:title) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalize_users(resource, key)
|
|
95
|
+
nodes = resource.dig(key, :nodes)
|
|
96
|
+
return {} unless nodes
|
|
97
|
+
|
|
98
|
+
{ key => nodes.map { |node| node.merge(id: extract_id_from_global_id(node[:id])) } }
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
def extract_id_from_global_id(global_id)
|
|
@@ -25,10 +25,17 @@ module Gitlab
|
|
|
25
25
|
}
|
|
26
26
|
GRAPHQL
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# Neither user field exists on every type in GitLab's GraphQL schema: Epic
|
|
29
|
+
# carries no assignees, and reviewers belong to MergeRequest alone.
|
|
30
|
+
# Selecting a field the type lacks makes the whole query fail to parse.
|
|
31
|
+
ASSIGNEES_RESOURCE_TYPES = %w[issues merge_requests].freeze
|
|
32
|
+
REVIEWERS_RESOURCE_TYPES = %w[merge_requests].freeze
|
|
33
|
+
|
|
34
|
+
def initialize(source_type, resource_type, conditions, graphql_only: false, actions: {})
|
|
29
35
|
@source_type = source_type.to_s.singularize
|
|
30
36
|
@resource_type = resource_type
|
|
31
37
|
@conditions = conditions
|
|
38
|
+
@actions = actions || {}
|
|
32
39
|
@graphql_only = graphql_only
|
|
33
40
|
@resource_declarations = [
|
|
34
41
|
'$source: ID!',
|
|
@@ -60,7 +67,7 @@ module Gitlab
|
|
|
60
67
|
|
|
61
68
|
private
|
|
62
69
|
|
|
63
|
-
attr_reader :source_type, :resource_type, :conditions, :graphql_only, :resource_declarations
|
|
70
|
+
attr_reader :source_type, :resource_type, :conditions, :actions, :graphql_only, :resource_declarations
|
|
64
71
|
|
|
65
72
|
def vote_attribute
|
|
66
73
|
@vote_attribute ||= (conditions.dig(:votes, :attribute) || conditions.dig(:upvotes, :attribute)).to_s
|
|
@@ -75,7 +82,8 @@ module Gitlab
|
|
|
75
82
|
if graphql_only
|
|
76
83
|
fields << 'labels { nodes { title } }'
|
|
77
84
|
fields << 'author { id name username }'
|
|
78
|
-
fields << 'assignees { nodes { id name username } }' if
|
|
85
|
+
fields << 'assignees { nodes { id name username } }' if query_assignees?
|
|
86
|
+
fields << 'reviewers { nodes { id name username } }' if query_reviewers?
|
|
79
87
|
fields << 'upvotes' if vote_attribute == 'upvotes'
|
|
80
88
|
fields << 'downvotes' if vote_attribute == 'downvotes'
|
|
81
89
|
fields.push('draft', 'mergedAt') if resource_type == 'merge_requests'
|
|
@@ -84,6 +92,41 @@ module Gitlab
|
|
|
84
92
|
fields
|
|
85
93
|
end
|
|
86
94
|
|
|
95
|
+
def query_assignees?
|
|
96
|
+
return false unless ASSIGNEES_RESOURCE_TYPES.include?(resource_type)
|
|
97
|
+
|
|
98
|
+
conditions.key?(:assignee_member) || actions_need?(:assignees)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def query_reviewers?
|
|
102
|
+
return false unless REVIEWERS_RESOURCE_TYPES.include?(resource_type)
|
|
103
|
+
|
|
104
|
+
actions_need?(:reviewers)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# An action needs a user field when it removes from that field, or when its
|
|
108
|
+
# text carries the field's placeholder. Either way the users have to be on
|
|
109
|
+
# the resource before the action runs, so the rule's actions decide these
|
|
110
|
+
# fields as much as its conditions do. The placeholder test matches the one
|
|
111
|
+
# in TextContentBuilder#replace_placeholders, so a policy that reaches for
|
|
112
|
+
# the users through Ruby interpolation instead is not covered.
|
|
113
|
+
def actions_need?(field)
|
|
114
|
+
actions[:"remove_#{field}"].present? || action_text.include?("{{#{field}}}")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def action_text
|
|
118
|
+
@action_text ||= collect_strings(actions).join("\n")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def collect_strings(value)
|
|
122
|
+
case value
|
|
123
|
+
when String then [value]
|
|
124
|
+
when Hash then value.values.flat_map { |nested| collect_strings(nested) }
|
|
125
|
+
when Array then value.flat_map { |nested| collect_strings(nested) }
|
|
126
|
+
else []
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
87
130
|
def resource_query
|
|
88
131
|
condition_queries = []
|
|
89
132
|
|
|
@@ -86,6 +86,30 @@ module Gitlab
|
|
|
86
86
|
}
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
def put(token, url, body)
|
|
90
|
+
response = HTTParty.put(
|
|
91
|
+
url,
|
|
92
|
+
body: body.to_json,
|
|
93
|
+
headers: {
|
|
94
|
+
'User-Agent' => USER_AGENT,
|
|
95
|
+
'Content-type' => 'application/json',
|
|
96
|
+
'PRIVATE-TOKEN' => token
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
raise_on_unauthorized_error!(response)
|
|
101
|
+
raise_on_internal_server_error!(response)
|
|
102
|
+
raise_on_too_many_requests!(response)
|
|
103
|
+
|
|
104
|
+
tracker&.record('PUT', url)
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
results: response.parsed_response,
|
|
108
|
+
ratelimit_remaining: response.headers['ratelimit-remaining'].to_i,
|
|
109
|
+
ratelimit_reset_at: Time.at(response.headers['ratelimit-reset'].to_i)
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
89
113
|
def delete(token, url)
|
|
90
114
|
response = HTTParty.delete(
|
|
91
115
|
url,
|
|
@@ -6,6 +6,11 @@ module Gitlab
|
|
|
6
6
|
class BasePolicy
|
|
7
7
|
InvalidPolicyError = Class.new(StandardError)
|
|
8
8
|
|
|
9
|
+
UPDATE_ACTION_KEYS = %i[
|
|
10
|
+
labels remove_labels assignees reviewers remove_assignees remove_reviewers
|
|
11
|
+
milestone due_date weight health_status title status discussion_locked move
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
9
14
|
attr_reader :type, :policy_spec, :resources, :network
|
|
10
15
|
attr_accessor :summary
|
|
11
16
|
|
|
@@ -55,8 +60,11 @@ module Gitlab
|
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
def comment?
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
actions.key?(:comment) || actions.key?(:mention)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def update?
|
|
67
|
+
(actions.keys.map(&:to_sym) & UPDATE_ACTION_KEYS).any?
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
def issue?
|
|
@@ -9,27 +9,37 @@ module Gitlab
|
|
|
9
9
|
module Policies
|
|
10
10
|
class RulePolicy < BasePolicy
|
|
11
11
|
# Build a summary from a single rule policy
|
|
12
|
+
#
|
|
13
|
+
# The builder is kept for the life of the policy. Every action that asks
|
|
14
|
+
# for it gets the same one, so the title and description templates are
|
|
15
|
+
# evaluated once for the run rather than once per action.
|
|
12
16
|
def build_summary
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
EntityBuilders::SummaryBuilder.new(
|
|
17
|
+
@build_summary ||= EntityBuilders::SummaryBuilder.new(
|
|
16
18
|
type: type,
|
|
17
19
|
policy_spec: policy_spec,
|
|
18
|
-
action:
|
|
20
|
+
action: actions.fetch(:summarize, {}),
|
|
19
21
|
resources: resources,
|
|
20
22
|
network: network)
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# The builder for one resource, kept for the life of the policy the same
|
|
26
|
+
# way build_summary is. Keyed on the resource itself, which the policy
|
|
27
|
+
# holds for the whole run, so two actions handed the same resource share
|
|
28
|
+
# one builder.
|
|
23
29
|
def build_issue(resource)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
EntityBuilders::IssueBuilder.new(
|
|
30
|
+
issue_builders[resource] ||= EntityBuilders::IssueBuilder.new(
|
|
27
31
|
type: type,
|
|
28
32
|
policy_spec: policy_spec,
|
|
29
|
-
action:
|
|
33
|
+
action: actions.fetch(:issue, {}),
|
|
30
34
|
resource: resource,
|
|
31
35
|
network: network)
|
|
32
36
|
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def issue_builders
|
|
41
|
+
@issue_builders ||= {}.compare_by_identity
|
|
42
|
+
end
|
|
33
43
|
end
|
|
34
44
|
end
|
|
35
45
|
end
|
|
@@ -8,7 +8,16 @@ module Gitlab
|
|
|
8
8
|
module Policies
|
|
9
9
|
class SummaryPolicy < BasePolicy
|
|
10
10
|
# Build a summary from several rules policies
|
|
11
|
+
#
|
|
12
|
+
# Kept for the life of the policy, like the single-rule version, so the
|
|
13
|
+
# child summaries are built once for the run.
|
|
11
14
|
def build_summary
|
|
15
|
+
@build_summary ||= build_combined_summary
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def build_combined_summary
|
|
12
21
|
action = actions[:summarize]
|
|
13
22
|
child_summaries = resources.map do |inner_policy_spec, inner_resources|
|
|
14
23
|
Policies::RulePolicy.new(
|
|
@@ -112,6 +112,28 @@ module Gitlab
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
def put_api(url, body)
|
|
116
|
+
response = execute_with_retry(
|
|
117
|
+
exception_types: [Net::ReadTimeout, Errors::Network::InternalServerError],
|
|
118
|
+
backoff_exceptions: Errors::Network::TooManyRequests, debug: options.debug) do
|
|
119
|
+
puts Gitlab::Triage::UI.debug "put_api: #{url}" if options.debug
|
|
120
|
+
|
|
121
|
+
@adapter.put(token, url, body)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
rate_limit_debug(response) if options.debug
|
|
125
|
+
rate_limit_wait(response)
|
|
126
|
+
|
|
127
|
+
results = response.delete(:results)
|
|
128
|
+
|
|
129
|
+
case results
|
|
130
|
+
when Hash
|
|
131
|
+
results.with_indifferent_access
|
|
132
|
+
else
|
|
133
|
+
raise_unexpected_response(results)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
115
137
|
def delete_api(url)
|
|
116
138
|
response = execute_with_retry(
|
|
117
139
|
exception_types: [Net::ReadTimeout, Errors::Network::InternalServerError],
|
|
@@ -17,9 +17,7 @@ module Gitlab
|
|
|
17
17
|
'Use the issues policy type for comment actions.'
|
|
18
18
|
|
|
19
19
|
# Action keys Action.actions_for dispatches for work_items policies.
|
|
20
|
-
#
|
|
21
|
-
# comment fallback for unrecognized keys, so anything outside this
|
|
22
|
-
# list would silently do nothing - reject it up front instead.
|
|
20
|
+
# Anything outside this list would silently do nothing, so reject it up front.
|
|
23
21
|
SUPPORTED_WORK_ITEM_ACTIONS = %i[labels assignees summarize comment_on_summary].freeze
|
|
24
22
|
|
|
25
23
|
def initialize(resource_rules)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-triage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.55.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -245,6 +245,7 @@ files:
|
|
|
245
245
|
- lib/gitlab/triage/action/delete.rb
|
|
246
246
|
- lib/gitlab/triage/action/issue.rb
|
|
247
247
|
- lib/gitlab/triage/action/summarize.rb
|
|
248
|
+
- lib/gitlab/triage/action/update.rb
|
|
248
249
|
- lib/gitlab/triage/action/work_item.rb
|
|
249
250
|
- lib/gitlab/triage/action/work_item_status.rb
|
|
250
251
|
- lib/gitlab/triage/api_observability.rb
|
|
@@ -255,13 +256,10 @@ files:
|
|
|
255
256
|
- lib/gitlab/triage/command_builders/base_command_builder.rb
|
|
256
257
|
- lib/gitlab/triage/command_builders/cc_command_builder.rb
|
|
257
258
|
- lib/gitlab/triage/command_builders/comment_command_builder.rb
|
|
258
|
-
- lib/gitlab/triage/command_builders/label_command_builder.rb
|
|
259
|
-
- lib/gitlab/triage/command_builders/move_command_builder.rb
|
|
260
|
-
- lib/gitlab/triage/command_builders/remove_label_command_builder.rb
|
|
261
|
-
- lib/gitlab/triage/command_builders/status_command_builder.rb
|
|
262
259
|
- lib/gitlab/triage/command_builders/text_content_builder.rb
|
|
263
260
|
- lib/gitlab/triage/engine.rb
|
|
264
261
|
- lib/gitlab/triage/entity_builders/issue_builder.rb
|
|
262
|
+
- lib/gitlab/triage/entity_builders/issue_fields.rb
|
|
265
263
|
- lib/gitlab/triage/entity_builders/summary_builder.rb
|
|
266
264
|
- lib/gitlab/triage/errors.rb
|
|
267
265
|
- lib/gitlab/triage/errors/network.rb
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_command_builder'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module Triage
|
|
7
|
-
module CommandBuilders
|
|
8
|
-
class LabelCommandBuilder < BaseCommandBuilder
|
|
9
|
-
def build_command
|
|
10
|
-
ensure_labels_exist!
|
|
11
|
-
|
|
12
|
-
super
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def ensure_labels_exist!
|
|
18
|
-
items.each do |label|
|
|
19
|
-
source_id_key = resource.key?(:group_id) ? :group_id : :project_id
|
|
20
|
-
label_opts = {
|
|
21
|
-
source_id_key => resource[source_id_key],
|
|
22
|
-
name: label
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
unless Resource::Label.new(label_opts, network: network).exist?
|
|
26
|
-
raise Resource::Label::LabelDoesntExistError,
|
|
27
|
-
"Label `#{label}` doesn't exist!"
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def slash_command_string
|
|
33
|
-
"/label"
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def format_item(item)
|
|
37
|
-
"~\"#{item}\""
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_command_builder'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module Triage
|
|
7
|
-
module CommandBuilders
|
|
8
|
-
class MoveCommandBuilder < BaseCommandBuilder
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def slash_command_string
|
|
12
|
-
"/move"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def format_item(item)
|
|
16
|
-
item
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_command_builder'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module Triage
|
|
7
|
-
module CommandBuilders
|
|
8
|
-
class RemoveLabelCommandBuilder < LabelCommandBuilder
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def slash_command_string
|
|
12
|
-
'/unlabel'
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'base_command_builder'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module Triage
|
|
7
|
-
module CommandBuilders
|
|
8
|
-
class StatusCommandBuilder < BaseCommandBuilder
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
def separator
|
|
12
|
-
''
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def slash_command_string
|
|
16
|
-
"/"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def format_item(item)
|
|
20
|
-
item
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|