Skip to content

Commit 5033a7a

Browse files
committed
Adding tasks to create general Slack channel and automate invite and kick users
1 parent 927a625 commit 5033a7a

12 files changed

Lines changed: 185 additions & 73 deletions

File tree

accounts/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ def user_type(self):
193193
else:
194194
# A non-specified group
195195
return None
196-
196+
@property
197+
def is_admin(self):
198+
return self.user_type in [UserType.SUPERUSER, UserType.STAFF, UserType.PARTNER_ADMIN, UserType.FACILITATOR_ADMIN]
197199

198200
class SlackSiteSettings(SingletonModel):
199201
""" Model to set how the showcase should be constructed"""

custom_slack_provider/slack.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class CustomSlackClient():
2828
user_detail_url = 'https://slack.com/api/users.info'
2929
create_conversation_url = 'https://slack.com/api/conversations.create'
3030
invite_conversation_url = 'https://slack.com/api/conversations.invite'
31+
kick_conversation_url = 'https://slack.com/api/conversations.kick'
3132
leave_conversation_url = 'https://slack.com/api/conversations.leave'
3233

3334
def __init__(self, token):
@@ -55,7 +56,10 @@ def _make_slack_post_request(self, url, data):
5556
return resp.json()
5657

5758
def get_identity(self):
58-
return self._make_slack_get_request(self.identity_url)
59+
identity = self._make_slack_get_request(self.identity_url)
60+
if not identity.get('ok'):
61+
raise SlackException(identity.get("error"))
62+
return identity
5963

6064
def leave_channel(self, channel):
6165
data = {
@@ -65,7 +69,7 @@ def leave_channel(self, channel):
6569
self.leave_conversation_url, data=data)
6670

6771
if not leave_channel.get('ok'):
68-
print(('An error occurred leaving a Slack Channel. '
72+
logger.error(('An error occurred leaving a Slack Channel. '
6973
f'Error code: {leave_channel.get("error")}'))
7074

7175
return leave_channel
@@ -107,7 +111,7 @@ def _extract_userid_from_username(self, username):
107111
when Slack is enabled and the account was created with a valid userid
108112
schema: [SLACK_USER_ID]_[WORKSPACE_TEAM_ID]"""
109113
if not re.match(r'[A-Z0-9]*[_]T[A-Z0-9]*', username):
110-
raise SlackException('Error adding user to channel')
114+
raise SlackException('Error adding user %s to channel' % username)
111115
return username.split('_')[0]
112116

113117
def invite_users_to_slack_channel(self, users, channel):
@@ -133,7 +137,7 @@ def kick_user_from_slack_channel(self, user, channel):
133137
"channel": channel,
134138
}
135139
user_added = self._make_slack_post_request(
136-
self.invite_conversation_url, data=data)
140+
self.kick_conversation_url, data=data)
137141

138142
if not user_added.get('ok'):
139143
return {

custom_slack_provider/tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test__make_slack_post_request(self, post):
6969

7070
@patch('custom_slack_provider.slack.CustomSlackClient._make_slack_get_request') # noqa: 501
7171
def test_get_identity(self, _make_slack_get_request):
72-
_make_slack_get_request.return_value = {'user': {'id': 1}}
72+
_make_slack_get_request.return_value = {'user': {'id': 1}, 'ok': True}
7373
client = CustomSlackClient(self.token)
7474
response = client.get_identity()
7575
self.assertEqual(response['user']['id'], 1)
@@ -84,7 +84,7 @@ def test__extract_userid_from_username(self):
8484
userid = client._extract_userid_from_username(invalid_username)
8585
except SlackException as e:
8686
self.assertTrue(isinstance(e, SlackException))
87-
self.assertEquals(e.message, 'Error adding user to channel')
87+
self.assertEquals(e.message, 'Error adding user bob@bob.com to channel')
8888

8989
@patch('custom_slack_provider.slack.CustomSlackClient._make_slack_post_request') # noqa: 501
9090
def test_invite_users_to_slack_channel(self, _make_slack_post_request):

hackathon/forms.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import forms
2+
from django.db.models import Q
23
from easy_select2 import Select2Multiple
34

45
from accounts.models import Organisation
@@ -125,7 +126,7 @@ class HackathonForm(forms.ModelForm):
125126
channel_admins = forms.ModelMultipleChoiceField(
126127
label="Channel Admins",
127128
required=False,
128-
queryset=User.objects.all(),
129+
queryset=User.objects.filter(Q(is_superuser=True) | Q(is_staff=True)),
129130
widget=Select2Multiple(select2attrs={'width': '100%'})
130131
)
131132

@@ -286,4 +287,4 @@ def save(self, commit=True):
286287
event.body += f'<br><br><b>Meeting Join Link:</b> <a href="{webinar_link}" target="_blank">Click here to join</a><br><b>Meeting Join Code:</b> {webinar_code}'
287288
if commit:
288289
event.save()
289-
return event
290+
return event
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.1.13 on 2024-09-11 13:06
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('hackathon', '0050_hackathon_google_registrations_form'),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name='hackathon',
15+
old_name='google_registrations_form',
16+
new_name='google_registration_form',
17+
),
18+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.1.13 on 2024-09-12 13:24
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('hackathon', '0051_auto_20240911_1306'),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name='hackathon',
15+
old_name='google_registration_form',
16+
new_name='registration_form',
17+
),
18+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 3.1.13 on 2024-09-12 15:27
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('hackathon', '0052_auto_20240912_1324'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='hackathon',
15+
name='is_register',
16+
),
17+
migrations.AddField(
18+
model_name='hackathon',
19+
name='allow_external_registrations',
20+
field=models.BooleanField(default=False),
21+
),
22+
]

hackathon/migrations/0051_auto_20240927_1539.py renamed to hackathon/migrations/0056_auto_20241011_1357.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 3.1.13 on 2024-09-27 15:39
1+
# Generated by Django 3.1.13 on 2024-10-11 13:57
22

33
from django.conf import settings
44
from django.db import migrations, models
@@ -8,24 +8,10 @@ class Migration(migrations.Migration):
88

99
dependencies = [
1010
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11-
('hackathon', '0050_hackathon_google_registrations_form'),
11+
('hackathon', '0055_remove_event_isreadonly'),
1212
]
1313

1414
operations = [
15-
migrations.RenameField(
16-
model_name='hackathon',
17-
old_name='google_registrations_form',
18-
new_name='registration_form',
19-
),
20-
migrations.RemoveField(
21-
model_name='hackathon',
22-
name='is_register',
23-
),
24-
migrations.AddField(
25-
model_name='hackathon',
26-
name='allow_external_registrations',
27-
field=models.BooleanField(default=False),
28-
),
2915
migrations.AddField(
3016
model_name='hackathon',
3117
name='channel_admins',

hackathon/tasks.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.core.mail import send_mail
88
from smtplib import SMTPException
99

10+
from accounts.models import CustomUser as User
1011
from accounts.models import EmailTemplate, SlackSiteSettings
1112

1213
from celery import shared_task
@@ -60,56 +61,76 @@ def create_new_hackathon_slack_channel(hackathon_id, channel_name):
6061
f"{hackathon.display_name} in Slack Workspace "
6162
f"{settings.SLACK_WORKSPACE}({settings.SLACK_TEAM_ID})"))
6263

64+
# Use a workspace admin's User Token to create the channel
65+
# This is required because a bot is seen as a Slack member and
66+
# if a workspace is set to only allow admins to create channels
67+
# creating the channel with a Bot Token will give an error
6368
admin_client = CustomSlackClient(settings.SLACK_ADMIN_TOKEN)
6469
channel_response = admin_client.create_slack_channel(
6570
team_id=settings.SLACK_TEAM_ID,
6671
channel_name=channel_name,
6772
is_private=True,
6873
)
6974

70-
if not channel_response['ok']:
71-
logger.error(channel_response['error'])
72-
7375
channel = channel_response.get('channel', {}).get('id')
7476
channel_url = f'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel}'
7577
hackathon.channel_url = channel_url
7678
hackathon.save()
7779
logger.info(f"Channel with id {channel} created.")
78-
80+
7981
# Add admins to channel for administration purposes
80-
users = [admin.username for admin in hackathon.channel_admins.all()]
81-
# First need to add Slack Bot to then add users to channel
82-
response = admin_client.invite_users_to_slack_channel(
83-
users=settings.SLACK_BOT_ID,
84-
channel=channel,
85-
)
86-
if not response['ok']:
87-
logger.error(response['error'])
88-
return
89-
90-
bot_client = CustomSlackClient(settings.SLACK_BOT_TOKEN)
82+
admin_usernames = [admin.username for admin in hackathon.channel_admins.all()]
9183
pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$')
92-
users_to_invite = ','.join([user.split('_')[0]
93-
for user in users if pattern.match(user)])
94-
bot_client.invite_users_to_slack_channel(
95-
users=users_to_invite,
84+
admin_user_ids = ','.join([username.split('_')[0]
85+
for username in admin_usernames
86+
if pattern.match(username)])
87+
admin_client.invite_users_to_slack_channel(
88+
users=admin_user_ids,
9689
channel=channel,
9790
)
9891

99-
if not response['ok']:
100-
logger.error(response['error'])
101-
return
102-
103-
if slack_site_settings.remove_admin_from_channel:
104-
# remove_admin_from_channel(users_to_invite, channel)
105-
pass
106-
10792

10893
@shared_task
10994
def invite_user_to_hackathon_slack_channel(hackathon_id, user_id):
110-
bot_client = CustomSlackClient(settings.SLACK_BOT_TOKEN)
95+
slack_site_settings = SlackSiteSettings.objects.first()
96+
if (not (settings.SLACK_ENABLED or settings.SLACK_BOT_TOKEN or settings.SLACK_ADMIN_TOKEN
97+
or settings.SLACK_WORKSPACE or not slack_site_settings)):
98+
logger.info("This feature is not enabeled.")
99+
return
100+
101+
hackathon = Hackathon.objects.get(id=hackathon_id)
102+
user = User.objects.get(id=user_id)
103+
logger.info(f"Inviting user {user_id} to hackathon {hackathon_id}'s slack channel")
104+
105+
admin_client = CustomSlackClient(settings.SLACK_ADMIN_TOKEN)
106+
channel = hackathon.channel_url.split('/')[-1]
107+
pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$')
108+
slack_user_id = admin_client._extract_userid_from_username(user.username)
109+
admin_client.invite_users_to_slack_channel(
110+
users=[slack_user_id],
111+
channel=channel
112+
)
113+
logger.info(f"Successfully invited user {user_id} to hackathon {hackathon_id}'s slack channel")
111114

112115

113116
@shared_task
114-
def kick_user_to_hackathon_slack_channel(user, channel):
115-
pass
117+
def kick_user_from_hackathon_slack_channel(hackathon_id, user_id):
118+
slack_site_settings = SlackSiteSettings.objects.first()
119+
if (not (settings.SLACK_ENABLED or settings.SLACK_BOT_TOKEN or settings.SLACK_ADMIN_TOKEN
120+
or settings.SLACK_WORKSPACE or not slack_site_settings)):
121+
logger.info("This feature is not enabeled.")
122+
return
123+
124+
hackathon = Hackathon.objects.get(id=hackathon_id)
125+
user = User.objects.get(id=user_id)
126+
logger.info(f"Kicking user {user_id} to hackathon {hackathon_id}'s slack channel")
127+
admin_client = CustomSlackClient(settings.SLACK_ADMIN_TOKEN)
128+
channel = hackathon.channel_url.split('/')[-1]
129+
pattern = re.compile(r'^U[a-zA-Z0-9]*[_]T[a-zA-Z0-9]*$')
130+
slack_user_id = admin_client._extract_userid_from_username(user.username)
131+
kicked = admin_client.kick_user_from_slack_channel(
132+
user=slack_user_id,
133+
channel=channel
134+
)
135+
logger.info(f"Successfully kicked user {user_id} to hackathon {hackathon_id}'s slack channel")
136+

hackathon/tests/task_tests.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
from unittest.mock import patch, Mock
77

88
from accounts.models import Organisation, CustomUser as User
9+
from accounts.models import SlackSiteSettings
910
from hackathon.models import Hackathon
10-
from hackathon.tasks import create_new_hackathon_slack_channel
11+
from hackathon.tasks import create_new_hackathon_slack_channel, \
12+
invite_user_to_hackathon_slack_channel, \
13+
kick_user_from_hackathon_slack_channel
1114

1215

1316
class TaskTests(TestCase):
@@ -18,17 +21,18 @@ def setUp(self):
1821
slack_display_name="bob",
1922
organisation=organisation,
2023
)
24+
self.slack_site_settings = SlackSiteSettings.objects.create(
25+
remove_admin_from_channel=True
26+
)
2127
self.hackathon = Hackathon.objects.create(
2228
created_by=self.user,
2329
display_name="hacktest",
2430
description="lorem ipsum",
2531
start_date=f'{datetime.now()}',
2632
end_date=f'{datetime.now()}')
2733
self.hackathon.channel_admins.add(self.user)
28-
29-
@override_settings(CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
30-
CELERY_ALWAYS_EAGER=True,
31-
BROKER_BACKEND='memory')
34+
35+
@responses.activate
3236
def test_create_new_hackathon_slack_channel(self):
3337
channel_id = 'CH123123'
3438
responses.add(
@@ -37,10 +41,45 @@ def test_create_new_hackathon_slack_channel(self):
3741
responses.add(
3842
responses.POST, 'https://slack.com/api/conversations.invite',
3943
json={'ok': True}, status=200)
40-
create_new_hackathon_slack_channel.apply_async(args=[
41-
self.hackathon.id, self.user.username])
44+
responses.add(
45+
responses.POST, 'https://slack.com/api/users.identity',
46+
json={'ok': True}, status=200)
47+
responses.add(
48+
responses.POST, 'https://slack.com/api/conversations.leave',
49+
json={'ok': True}, status=200)
4250

43-
import time; time.sleep(3)
51+
create_new_hackathon_slack_channel(self.hackathon.id, self.user.username)
52+
4453
self.assertEquals(
45-
self.hackathon.channel_url,
54+
Hackathon.objects.first().channel_url,
4655
f'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}')
56+
57+
@responses.activate
58+
def test_invite_user_to_channel(self):
59+
channel_id = 'CH123123'
60+
self.hackathon.channel_url = 'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}'
61+
self.hackathon.save()
62+
63+
responses.add(
64+
responses.POST, 'https://slack.com/api/conversations.invite',
65+
json={'ok': True}, status=200)
66+
67+
try:
68+
invite_user_to_hackathon_slack_channel(self.hackathon.id, self.user.id)
69+
except:
70+
raise Exception("Inviting user to channel failed")
71+
72+
@responses.activate
73+
def test_kick_user_from_channel(self):
74+
channel_id = 'CH123123'
75+
self.hackathon.channel_url = 'https://{settings.SLACK_WORKSPACE}.slack.com/archives/{channel_id}'
76+
self.hackathon.save()
77+
78+
responses.add(
79+
responses.POST, 'https://slack.com/api/conversations.kick',
80+
json={'ok': True}, status=200)
81+
82+
try:
83+
kick_user_from_hackathon_slack_channel(self.hackathon.id, self.user.id)
84+
except:
85+
raise Exception("Kicking user from channel failed")

0 commit comments

Comments
 (0)