Skip to content

Commit c5d8097

Browse files
committed
support inbound messaging
1 parent 3761ee7 commit c5d8097

File tree

3 files changed

+103
-17
lines changed

3 files changed

+103
-17
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
# Zammad Package for seven.io
5+
This package adds seven as a SMS provider for two-way messaging.
56

67
Tested with Zammad v6.x.
78

@@ -14,7 +15,13 @@ Tested with Zammad v6.x.
1415
6. Go to **Manage->Channels->SMS->SMS Notification** and choose **seven**
1516
7. Type in your [API Key](https://help.seven.io/en/api-key-access), test and you are ready to go
1617

17-
### Support
18+
## Usage
19+
20+
Please refer to the official [documentation](https://admin-docs.zammad.org/en/6.1/channels/sms.html) over at Zammad.
21+
22+
23+
24+
## Support
1825

1926
Need help? Feel free to [contact us](https://www.seven.io/en/company/contact/).
2027

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
class Channel::Driver::Sms::Seven < Channel::Driver::Sms::Base
22
NAME = 'sms/seven'.freeze
33

4+
def fetchable?(_channel)
5+
false
6+
end
7+
48
def deliver(options, attr, _notification = false)
59
Rails.logger.info "Sending SMS to recipient #{attr[:recipient]}"
610

@@ -9,14 +13,14 @@ def deliver(options, attr, _notification = false)
913
Rails.logger.info "Backend sending seven SMS to #{attr[:recipient]}"
1014
begin
1115
url = 'https://gateway.seven.io/api/sms?' + URI.encode_www_form({
12-
p: options[:api_key],
13-
text: attr[:message],
14-
to: attr[:recipient],
15-
from: options[:from],
16-
sendWith: 'zammad',
16+
p: options[:api_key],
17+
text: attr[:message],
18+
to: attr[:recipient],
19+
from: options[:from],
20+
sendWith: 'zammad',
1721
})
1822

19-
if Setting.get('developer_mode') != true
23+
if Setting.get('developer_mode') != false
2024
response = Faraday.get(url).body
2125
raise response if '100' != response
2226
end
@@ -28,14 +32,84 @@ def deliver(options, attr, _notification = false)
2832
end
2933
end
3034

35+
def process(_options, attr, channel)
36+
from = attr['data']['sender']
37+
38+
Rails.logger.info "Receiving SMS from recipient #{from}"
39+
40+
if from.sub('+', '').scan(/^\d+$/).empty?
41+
Rails.logger.info "Skipping inbound SMS because the sender is not a valid phone number: #{from}"
42+
return [:json, {}]
43+
end
44+
45+
# prevent already created articles
46+
if Ticket::Article.exists?(message_id: attr['data']['id'])
47+
return [:json, {}]
48+
end
49+
50+
# find sender
51+
user = user_by_mobile(from)
52+
UserInfo.current_user_id = user.id
53+
54+
process_ticket(attr, channel, user)
55+
56+
[:json, {}]
57+
end
58+
59+
def create_ticket(attr, channel, user)
60+
title = cut_title(attr['data']['text'])
61+
ticket = Ticket.new(
62+
group_id: channel.group_id,
63+
title: title,
64+
state_id: Ticket::State.find_by(default_create: true).id,
65+
priority_id: Ticket::Priority.find_by(default_create: true).id,
66+
customer_id: user.id,
67+
preferences: {
68+
channel_id: channel.id,
69+
sms: {
70+
originator: attr['data']['sender'],
71+
recipient: attr['data']['system'],
72+
}
73+
}
74+
)
75+
ticket.save!
76+
ticket
77+
end
78+
79+
def create_article(attr, channel, ticket)
80+
Ticket::Article.create!(
81+
ticket_id: ticket.id,
82+
type: article_type_sms,
83+
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
84+
body: attr['data']['text'],
85+
from: attr['data']['sender'],
86+
to: attr['data']['system'],
87+
message_id: attr['data']['id'],
88+
content_type: 'text/plain',
89+
preferences: {
90+
channel_id: channel.id,
91+
sms: {
92+
From: attr['data']['sender'],
93+
To: attr['data']['system'],
94+
},
95+
}
96+
)
97+
end
98+
3199
def self.definition
32100
{
33-
name: 'seven',
34-
adapter: 'sms/seven',
35-
notification: [
36-
{name: 'options::api_key', display: 'API Key', tag: 'input', type: 'text', limit: 64, null: false, placeholder: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'},
37-
{name: 'options::from', display: 'From', tag: 'input', type: 'text', limit: 16, null: true, placeholder: '00491710000000'},
38-
]
101+
name: 'seven',
102+
adapter: 'sms/seven',
103+
account: [
104+
{ name: 'options::webhook_token', display: __('Webhook Token'), tag: 'input', type: 'text', limit: 200, null: false, default: Digest::MD5.hexdigest(SecureRandom.uuid), disabled: true, readonly: true },
105+
{name: 'options::api_key', display: 'API Key', tag: 'input', type: 'text', limit: 64, null: false, placeholder: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'},
106+
{name: 'options::from', display: 'From', tag: 'input', type: 'text', limit: 16, null: true, placeholder: '00491710000000'},
107+
{ name: 'group_id', display: __('Destination Group'), tag: 'tree_select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
108+
],
109+
notification: [
110+
{name: 'options::api_key', display: 'API Key', tag: 'input', type: 'text', limit: 64, null: false, placeholder: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'},
111+
{name: 'options::from', display: 'From', tag: 'input', type: 'text', limit: 16, null: true, placeholder: '00491710000000'},
112+
]
39113
}
40114
end
41-
end
115+
end

seven-sms.szpm

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
{
22
"name": "seven SMS",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"vendor": "seven communications GmbH & Co. KG",
55
"license": "MIT",
66
"url": "https://www.seven.io/",
77
"buildhost": "localhost",
8-
"builddate": "2024-21-05 00:00:00 UTC",
8+
"builddate": "2025-26-03 00:00:00 UTC",
99
"change_log": [
10+
{
11+
"version": "2.1.0",
12+
"date": "2025-26-03 00:00:00 UTC",
13+
"log": "added inbound messaging"
14+
},
1015
{
1116
"version": "2.0.0",
1217
"date": "2024-21-05 00:00:00 UTC",
@@ -34,7 +39,7 @@
3439
"location": "app/models/channel/driver/sms/seven.rb",
3540
"permission": 644,
3641
"encode": "base64",
37-
"content": "Y2xhc3MgQ2hhbm5lbDo6RHJpdmVyOjpTbXM6OlNldmVuIDwgQ2hhbm5lbDo6RHJpdmVyOjpTbXM6OkJhc2UNCiAgTkFNRSA9ICdzbXMvc2V2ZW4nLmZyZWV6ZQ0KDQogIGRlZiBkZWxpdmVyKG9wdGlvbnMsIGF0dHIsIF9ub3RpZmljYXRpb24gPSBmYWxzZSkNCiAgICBSYWlscy5sb2dnZXIuaW5mbyAiU2VuZGluZyBTTVMgdG8gcmVjaXBpZW50ICN7YXR0cls6cmVjaXBpZW50XX0iDQoNCiAgICByZXR1cm4gdHJ1ZSBpZiBTZXR0aW5nLmdldCgnaW1wb3J0X21vZGUnKQ0KDQogICAgUmFpbHMubG9nZ2VyLmluZm8gIkJhY2tlbmQgc2VuZGluZyBzZXZlbiBTTVMgdG8gI3thdHRyWzpyZWNpcGllbnRdfSINCiAgICBiZWdpbg0KICAgICAgdXJsID0gJ2h0dHBzOi8vZ2F0ZXdheS5zZXZlbi5pby9hcGkvc21zPycgKyBVUkkuZW5jb2RlX3d3d19mb3JtKHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcDogb3B0aW9uc1s6YXBpX2tleV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRleHQ6IGF0dHJbOm1lc3NhZ2VdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0bzogYXR0cls6cmVjaXBpZW50XSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZnJvbTogb3B0aW9uc1s6ZnJvbV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbmRXaXRoOiAnemFtbWFkJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9KQ0KDQogICAgICBpZiBTZXR0aW5nLmdldCgnZGV2ZWxvcGVyX21vZGUnKSAhPSB0cnVlDQogICAgICAgIHJlc3BvbnNlID0gRmFyYWRheS5nZXQodXJsKS5ib2R5DQogICAgICAgIHJhaXNlIHJlc3BvbnNlIGlmICcxMDAnICE9IHJlc3BvbnNlDQogICAgICBlbmQNCg0KICAgICAgdHJ1ZQ0KICAgIHJlc2N1ZSA9PiBlDQogICAgICBSYWlscy5sb2dnZXIuZGVidWcgInNldmVuIGVycm9yOiAje2UuaW5zcGVjdH0iDQogICAgICByYWlzZSBlDQogICAgZW5kDQogIGVuZA0KDQogIGRlZiBzZWxmLmRlZmluaXRpb24NCiAgICB7DQogICAgICAgIG5hbWU6ICdzZXZlbicsDQogICAgICAgIGFkYXB0ZXI6ICdzbXMvc2V2ZW4nLA0KICAgICAgICBub3RpZmljYXRpb246IFsNCiAgICAgICAgICAgIHtuYW1lOiAnb3B0aW9uczo6YXBpX2tleScsIGRpc3BsYXk6ICdBUEkgS2V5JywgdGFnOiAnaW5wdXQnLCB0eXBlOiAndGV4dCcsIGxpbWl0OiA2NCwgbnVsbDogZmFsc2UsIHBsYWNlaG9sZGVyOiAnWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWCd9LA0KICAgICAgICAgICAge25hbWU6ICdvcHRpb25zOjpmcm9tJywgZGlzcGxheTogJ0Zyb20nLCB0YWc6ICdpbnB1dCcsIHR5cGU6ICd0ZXh0JywgbGltaXQ6IDE2LCBudWxsOiB0cnVlLCBwbGFjZWhvbGRlcjogJzAwNDkxNzEwMDAwMDAwJ30sDQogICAgICAgIF0NCiAgICB9DQogIGVuZA0KZW5k"
42+
"content": "Y2xhc3MgQ2hhbm5lbDo6RHJpdmVyOjpTbXM6OlNldmVuIDwgQ2hhbm5lbDo6RHJpdmVyOjpTbXM6OkJhc2UKICBOQU1FID0gJ3Ntcy9zZXZlbicuZnJlZXplCgogIGRlZiBmZXRjaGFibGU/KF9jaGFubmVsKQogICAgZmFsc2UKICBlbmQKCiAgZGVmIGRlbGl2ZXIob3B0aW9ucywgYXR0ciwgX25vdGlmaWNhdGlvbiA9IGZhbHNlKQogICAgUmFpbHMubG9nZ2VyLmluZm8gIlNlbmRpbmcgU01TIHRvIHJlY2lwaWVudCAje2F0dHJbOnJlY2lwaWVudF19IgoKICAgIHJldHVybiB0cnVlIGlmIFNldHRpbmcuZ2V0KCdpbXBvcnRfbW9kZScpCgogICAgUmFpbHMubG9nZ2VyLmluZm8gIkJhY2tlbmQgc2VuZGluZyBzZXZlbiBTTVMgdG8gI3thdHRyWzpyZWNpcGllbnRdfSIKICAgIGJlZ2luCiAgICAgIHVybCA9ICdodHRwczovL2dhdGV3YXkuc2V2ZW4uaW8vYXBpL3Ntcz8nICsgVVJJLmVuY29kZV93d3dfZm9ybSh7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHA6IG9wdGlvbnNbOmFwaV9rZXldLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0OiBhdHRyWzptZXNzYWdlXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdG86IGF0dHJbOnJlY2lwaWVudF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZyb206IG9wdGlvbnNbOmZyb21dLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZW5kV2l0aDogJ3phbW1hZCcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9KQoKICAgICAgaWYgU2V0dGluZy5nZXQoJ2RldmVsb3Blcl9tb2RlJykgIT0gZmFsc2UKICAgICAgICByZXNwb25zZSA9IEZhcmFkYXkuZ2V0KHVybCkuYm9keQogICAgICAgIHJhaXNlIHJlc3BvbnNlIGlmICcxMDAnICE9IHJlc3BvbnNlCiAgICAgIGVuZAoKICAgICAgdHJ1ZQogICAgcmVzY3VlID0+IGUKICAgICAgUmFpbHMubG9nZ2VyLmRlYnVnICJzZXZlbiBlcnJvcjogI3tlLmluc3BlY3R9IgogICAgICByYWlzZSBlCiAgICBlbmQKICBlbmQKCiAgZGVmIHByb2Nlc3MoX29wdGlvbnMsIGF0dHIsIGNoYW5uZWwpCiAgICBmcm9tID0gYXR0clsnZGF0YSddWydzZW5kZXInXQoKICAgIFJhaWxzLmxvZ2dlci5pbmZvICJSZWNlaXZpbmcgU01TIGZyb20gcmVjaXBpZW50ICN7ZnJvbX0iCgogICAgaWYgZnJvbS5zdWIoJysnLCAnJykuc2NhbigvXlxkKyQvKS5lbXB0eT8KICAgICAgUmFpbHMubG9nZ2VyLmluZm8gIlNraXBwaW5nIGluYm91bmQgU01TIGJlY2F1c2UgdGhlIHNlbmRlciBpcyBub3QgYSB2YWxpZCBwaG9uZSBudW1iZXI6ICN7ZnJvbX0iCiAgICAgIHJldHVybiBbOmpzb24sIHt9XQogICAgZW5kCgogICAgIyBwcmV2ZW50IGFscmVhZHkgY3JlYXRlZCBhcnRpY2xlcwogICAgaWYgVGlja2V0OjpBcnRpY2xlLmV4aXN0cz8obWVzc2FnZV9pZDogYXR0clsnZGF0YSddWydpZCddKQogICAgICByZXR1cm4gWzpqc29uLCB7fV0KICAgIGVuZAoKICAgICMgZmluZCBzZW5kZXIKICAgIHVzZXIgPSB1c2VyX2J5X21vYmlsZShmcm9tKQogICAgVXNlckluZm8uY3VycmVudF91c2VyX2lkID0gdXNlci5pZAoKICAgIHByb2Nlc3NfdGlja2V0KGF0dHIsIGNoYW5uZWwsIHVzZXIpCgogICAgWzpqc29uLCB7fV0KICBlbmQKCiAgZGVmIGNyZWF0ZV90aWNrZXQoYXR0ciwgY2hhbm5lbCwgdXNlcikKICAgIHRpdGxlID0gY3V0X3RpdGxlKGF0dHJbJ2RhdGEnXVsndGV4dCddKQogICAgdGlja2V0ID0gVGlja2V0Lm5ldygKICAgICAgZ3JvdXBfaWQ6ICAgIGNoYW5uZWwuZ3JvdXBfaWQsCiAgICAgIHRpdGxlOiAgICAgICB0aXRsZSwKICAgICAgc3RhdGVfaWQ6ICAgIFRpY2tldDo6U3RhdGUuZmluZF9ieShkZWZhdWx0X2NyZWF0ZTogdHJ1ZSkuaWQsCiAgICAgIHByaW9yaXR5X2lkOiBUaWNrZXQ6OlByaW9yaXR5LmZpbmRfYnkoZGVmYXVsdF9jcmVhdGU6IHRydWUpLmlkLAogICAgICBjdXN0b21lcl9pZDogdXNlci5pZCwKICAgICAgcHJlZmVyZW5jZXM6IHsKICAgICAgICBjaGFubmVsX2lkOiBjaGFubmVsLmlkLAogICAgICAgIHNtczogICAgICAgIHsKICAgICAgICAgIG9yaWdpbmF0b3I6IGF0dHJbJ2RhdGEnXVsnc2VuZGVyJ10sCiAgICAgICAgICByZWNpcGllbnQ6ICBhdHRyWydkYXRhJ11bJ3N5c3RlbSddLAogICAgICAgIH0KICAgICAgfQogICAgKQogICAgdGlja2V0LnNhdmUhCiAgICB0aWNrZXQKICBlbmQKCiAgZGVmIGNyZWF0ZV9hcnRpY2xlKGF0dHIsIGNoYW5uZWwsIHRpY2tldCkKICAgIFRpY2tldDo6QXJ0aWNsZS5jcmVhdGUhKAogICAgICB0aWNrZXRfaWQ6ICAgIHRpY2tldC5pZCwKICAgICAgdHlwZTogICAgICAgICBhcnRpY2xlX3R5cGVfc21zLAogICAgICBzZW5kZXI6ICAgICAgIFRpY2tldDo6QXJ0aWNsZTo6U2VuZGVyLmZpbmRfYnkobmFtZTogJ0N1c3RvbWVyJyksCiAgICAgIGJvZHk6ICAgICAgICAgYXR0clsnZGF0YSddWyd0ZXh0J10sCiAgICAgIGZyb206ICAgICAgICAgYXR0clsnZGF0YSddWydzZW5kZXInXSwKICAgICAgdG86ICAgICAgICAgICBhdHRyWydkYXRhJ11bJ3N5c3RlbSddLAogICAgICBtZXNzYWdlX2lkOiAgIGF0dHJbJ2RhdGEnXVsnaWQnXSwKICAgICAgY29udGVudF90eXBlOiAndGV4dC9wbGFpbicsCiAgICAgIHByZWZlcmVuY2VzOiAgewogICAgICAgIGNoYW5uZWxfaWQ6IGNoYW5uZWwuaWQsCiAgICAgICAgc21zOiAgICAgICAgewogICAgICAgICAgRnJvbTogICAgICAgYXR0clsnZGF0YSddWydzZW5kZXInXSwKICAgICAgICAgIFRvOiAgICAgICAgIGF0dHJbJ2RhdGEnXVsnc3lzdGVtJ10sCiAgICAgICAgfSwKICAgICAgfQogICAgKQogIGVuZAoKICBkZWYgc2VsZi5kZWZpbml0aW9uCiAgICB7CiAgICAgIG5hbWU6ICdzZXZlbicsCiAgICAgIGFkYXB0ZXI6ICdzbXMvc2V2ZW4nLAogICAgICBhY2NvdW50OiAgICAgIFsKICAgICAgICB7IG5hbWU6ICdvcHRpb25zOjp3ZWJob29rX3Rva2VuJywgZGlzcGxheTogX18oJ1dlYmhvb2sgVG9rZW4nKSwgdGFnOiAnaW5wdXQnLCB0eXBlOiAndGV4dCcsIGxpbWl0OiAyMDAsIG51bGw6IGZhbHNlLCBkZWZhdWx0OiBEaWdlc3Q6Ok1ENS5oZXhkaWdlc3QoU2VjdXJlUmFuZG9tLnV1aWQpLCBkaXNhYmxlZDogdHJ1ZSwgcmVhZG9ubHk6IHRydWUgfSwKICAgICAgICB7bmFtZTogJ29wdGlvbnM6OmFwaV9rZXknLCBkaXNwbGF5OiAnQVBJIEtleScsIHRhZzogJ2lucHV0JywgdHlwZTogJ3RleHQnLCBsaW1pdDogNjQsIG51bGw6IGZhbHNlLCBwbGFjZWhvbGRlcjogJ1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFgnfSwKICAgICAgICB7bmFtZTogJ29wdGlvbnM6OmZyb20nLCBkaXNwbGF5OiAnRnJvbScsIHRhZzogJ2lucHV0JywgdHlwZTogJ3RleHQnLCBsaW1pdDogMTYsIG51bGw6IHRydWUsIHBsYWNlaG9sZGVyOiAnMDA0OTE3MTAwMDAwMDAnfSwKICAgICAgICB7IG5hbWU6ICdncm91cF9pZCcsIGRpc3BsYXk6IF9fKCdEZXN0aW5hdGlvbiBHcm91cCcpLCB0YWc6ICd0cmVlX3NlbGVjdCcsIG51bGw6IGZhbHNlLCByZWxhdGlvbjogJ0dyb3VwJywgbnVsbG9wdGlvbjogdHJ1ZSwgZmlsdGVyOiB7IGFjdGl2ZTogdHJ1ZSB9IH0sCiAgICAgIF0sCiAgICAgIG5vdGlmaWNhdGlvbjogWwogICAgICAgIHtuYW1lOiAnb3B0aW9uczo6YXBpX2tleScsIGRpc3BsYXk6ICdBUEkgS2V5JywgdGFnOiAnaW5wdXQnLCB0eXBlOiAndGV4dCcsIGxpbWl0OiA2NCwgbnVsbDogZmFsc2UsIHBsYWNlaG9sZGVyOiAnWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWCd9LAogICAgICAgIHtuYW1lOiAnb3B0aW9uczo6ZnJvbScsIGRpc3BsYXk6ICdGcm9tJywgdGFnOiAnaW5wdXQnLCB0eXBlOiAndGV4dCcsIGxpbWl0OiAxNiwgbnVsbDogdHJ1ZSwgcGxhY2Vob2xkZXI6ICcwMDQ5MTcxMDAwMDAwMCd9LAogICAgICBdCiAgICB9CiAgZW5kCmVuZAo="
3843
}
3944
]
4045
}

0 commit comments

Comments
 (0)