Skip to content
Snippets Groups Projects
Commit 58c6cdb0 authored by Tamas Kiss's avatar Tamas Kiss
Browse files

Refactoring match making

parent 73bba44b
Branches
Tags 0.0.2
No related merge requests found
# Mattermost Coffee Bot # Mattermost Coffee Bot
The purpose of this small bot is to act as a matchmaker for users that shares channel with the bot. The purpose of this small bot is to act as a matchmaker for users that shares channel with the bot.
## Run
Currently the bot only supports one entrypoint when it will run in every channel it knows and make pairs of 2 for every channel. Also, the messages are currently hard-coded in Hungarian, and custom-tailored to KSZK's Mattermost Installation.
To run this entrypoint
```
. ./virtualenv/bin/activate
MATTERMOST_URL=<mattermost-url> MATTERMOST_TOKEN=<mattermost-token> ./coffee_bot/run_for_all_channels.py
```
## Development ## Development
To set up a local development environment, run: To set up a local development environment, run:
``` ```
......
import copy import copy
import random import random
from typing import Generator, Iterable from typing import Generator, Iterable, Tuple, List
import mattermost import mattermost
#pylint: disable=invalid-name #pylint: disable=invalid-name
...@@ -26,77 +26,58 @@ def random_nonrepeating_tuples(population: Iterable, n: int) -> Generator[tuple, ...@@ -26,77 +26,58 @@ def random_nonrepeating_tuples(population: Iterable, n: int) -> Generator[tuple,
yield tuple(buffer) yield tuple(buffer)
def matching(client: mattermost.MMApi) -> None: # pylint: disable=too-many-arguments
def matching_in_channel(client: mattermost.MMApi, channel_id: str,
group_hail=None, left_out_message=None, too_few_message=None,
minimum_members=4, group_size=2) -> None:
""" """
Run through every channel associated with the client and pairs channel members into group chats. Get members of a channel, make matches between these members and handle communication regarding these actions
""" """
teams = get_my_teams(client)
for team in teams:
team_channels = client.get_channel_memberships_for_user(
'me', team['id'])
for channel in team_channels:
channel_id = channel['channel_id']
members = members_in_channel_besides_me(client, channel_id) members = members_in_channel_besides_me(client, channel_id)
if len(members) <= 4: if len(members) < minimum_members:
# Too few members in this channel to make meaningful groups if too_few_message:
continue client.create_post(channel_id=channel_id, message=too_few_message)
return
make_match_in_channel(client, channel_id, members) groups_created, left_out = make_matches_for_members(client, members, group_size)
if group_hail:
for group_id in groups_created:
client.create_post(channel_id=group_id, message=group_hail)
def handle_no_pair(client: mattermost.MMApi, channel_id: str, group: Iterable[dict]) -> None: if left_out_message and left_out is not None:
""" user_data = client.get_user(left_out)
Handle if someone should be left out because they can not paired up client.create_post(channel_id=channel_id, message=left_out_message.format(user=user_data))
It sends a message to the channel it runs for so others know if someone had to be left out.
"""
user = get_user_data(client, group[0]["user_id"])
client.create_post(
channel_id=channel_id,
message=f"Ebben a körben {user['last_name']} {user['first_name']} nem került sorra sajnos.",
)
def create_group(client: mattermost.MMApi, ids: Iterable[str]) -> None:
def create_group(client: mattermost.MMApi, group: Iterable[dict]) -> None:
""" """
Creating a group-message for the group Creating a group-message for the group
Creates a new group-message between the group-members (and the bot itself),
and sends a short welcome message.
""" """
ids = [m['user_id'] for m in group]
groupchat = client.create_group_channel_with(other_user_ids_list=ids) groupchat = client.create_group_channel_with(other_user_ids_list=ids)
client.create_post(
channel_id=groupchat['id'],
message="Most ti kerültetek sorra. Jó beszélgetést és kávézást! :coffee_parrot:",
)
return groupchat['id']
def make_match_in_channel(client: mattermost.MMApi, channel_id: str, members: Iterable[dict],
groupsize: int=2) -> None: def make_matches_for_members(client: mattermost.MMApi, members: Iterable[dict],
group_size: int = 2) -> Tuple[List[str], str]:
""" """
Making matches in a channel Making matches in a channel
""" """
for group in random_nonrepeating_tuples(members, groupsize): groups_created = []
left_out = None
for group in random_nonrepeating_tuples(members, group_size):
if len(group) == 1: if len(group) == 1:
handle_no_pair(client, channel_id, group) left_out = group[0]['user_id']
else: else:
create_group(client, group) groups_created.append(create_group(client, [u['user_id'] for u in group]))
return groups_created, left_out
def get_my_teams(client):
"""
Get a list of the teams the bot associated with the client has access to.
Note: This is function is *not* available in the imported mattermost API binding lib.
"""
# pylint: disable=protected-access
return client._get('/v4/users/me/teams')
def members_in_channel_besides_me(client, channel_id): def members_in_channel_besides_me(client, channel_id):
...@@ -107,13 +88,3 @@ def members_in_channel_besides_me(client, channel_id): ...@@ -107,13 +88,3 @@ def members_in_channel_besides_me(client, channel_id):
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
return [m for m in client.get_channel_members(channel_id) if m['user_id'] != client._my_user_id] return [m for m in client.get_channel_members(channel_id) if m['user_id'] != client._my_user_id]
def get_user_data(client, user_id):
"""
Gets user data for the given user id using the given client.
Note: This is function is *not* properly implemented by the imported mattermost API binding lib.
"""
# pylint: disable=protected-access
return client._get(f'/v4/users/{user_id}')
import mattermost
from .config import MATTERMOST_TOKEN, MATTERMOST_URL
from .coffee_bot import matching
if __name__ == "__main__":
client = mattermost.MMApi(MATTERMOST_URL)
client.login(bearer=MATTERMOST_TOKEN)
matching(client)
...@@ -4,10 +4,10 @@ import copy ...@@ -4,10 +4,10 @@ import copy
import mock import mock
import mattermost import mattermost
from coffee_bot import coffee_bot from coffee_bot import match_making
class MatchmakingCase(unittest.TestCase): class MatchMakingCaseBase(unittest.TestCase):
def setUp(self): def setUp(self):
self.my_user_id = "my-bots-user-id" self.my_user_id = "my-bots-user-id"
self.client = mock.MagicMock(mattermost.MMApi) self.client = mock.MagicMock(mattermost.MMApi)
...@@ -19,77 +19,6 @@ class MatchmakingCase(unittest.TestCase): ...@@ -19,77 +19,6 @@ class MatchmakingCase(unittest.TestCase):
self.my_channel_membership = self._create_membership_object( self.my_channel_membership = self._create_membership_object(
self.my_user_id) self.my_user_id)
def test_exclude_myself(self):
members = self._generate_n_members(self.minimum_membership_requirement)
self._set_channel_members(members, add_myself=True)
self.assertIn(self.my_channel_membership, self.members)
members_except_me = coffee_bot.members_in_channel_besides_me(
self.client, "channel-id")
self.assertNotIn(
self.my_channel_membership, members_except_me)
self.assertListEqual(members_except_me, members)
@mock.patch('coffee_bot.coffee_bot.handle_no_pair')
@mock.patch('coffee_bot.coffee_bot.create_group')
def test_even_members(self, create_group, handle_no_pair):
# pylint: disable=invalid-name
n = (self.minimum_membership_requirement + 1) * 2
members = self._generate_n_members(n)
coffee_bot.make_match_in_channel(self.client, "channel-id", members)
self.assertEqual(create_group.call_count,
self.minimum_membership_requirement + 1)
self.assertEqual(handle_no_pair.call_count, 0)
@mock.patch('coffee_bot.coffee_bot.handle_no_pair')
@mock.patch('coffee_bot.coffee_bot.create_group')
def test_odd_members(self, create_group, handle_no_pair):
# pylint: disable=invalid-name
n = (self.minimum_membership_requirement * 2) + 1
members = self._generate_n_members(n)
coffee_bot.make_match_in_channel(self.client, "channel-id", members)
self.assertEqual(create_group.call_count,
self.minimum_membership_requirement)
self.assertEqual(handle_no_pair.call_count, 1)
def test_group_creation(self):
members = self._generate_n_members(2)
self.client.create_group_channel_with.return_value = {
"id": "group-chat-id"}
coffee_bot.create_group(self.client, members)
self.client.create_group_channel_with.assert_called_with(
other_user_ids_list=['user-id-0', 'user-id-1']
)
self.client.create_post.assert_called_with(
channel_id="group-chat-id",
message="Most ti kerültetek sorra. Jó beszélgetést és kávézást! :coffee_parrot:",
)
def test_handling_no_pairs(self):
user_id = "user-id-1"
member = self._create_membership_object(user_id)
user_data = {'id': user_id, 'username': "UserId1",
"first_name": "User", "last_name": "Generated"}
with mock.patch('coffee_bot.coffee_bot.get_user_data', return_value=user_data) as get_user_data:
coffee_bot.handle_no_pair(self.client, "channel-id", (member,))
get_user_data.assert_called_with(self.client, user_id)
self.client.create_post.assert_called_with(
channel_id="channel-id",
message="Ebben a körben Generated User nem került sorra sajnos.",
)
def _set_channel_members(self, members, add_myself=True): def _set_channel_members(self, members, add_myself=True):
self.members = copy.deepcopy(members) self.members = copy.deepcopy(members)
...@@ -119,10 +48,157 @@ class MatchmakingCase(unittest.TestCase): ...@@ -119,10 +48,157 @@ class MatchmakingCase(unittest.TestCase):
"explicit_roles": "", "explicit_roles": "",
} }
def _generate_n_members(self, n, **kwargs): # pylint: disable=invalid-name @classmethod
def _generate_n_members(cls, n, **kwargs): # pylint: disable=invalid-name
members = [] members = []
for i in range(0, n): for i in range(0, n):
members.append(self._create_membership_object( members.append(cls._create_membership_object(
f"user-id-{i}", **kwargs)) f"user-id-{i}", **kwargs))
return members return members
class MatchMakingCase(MatchMakingCaseBase):
def test_exclude_myself(self):
members = self._generate_n_members(self.minimum_membership_requirement)
self._set_channel_members(members, add_myself=True)
self.assertIn(self.my_channel_membership, self.members)
members_except_me = match_making.members_in_channel_besides_me(
self.client, "channel-id")
self.assertNotIn(
self.my_channel_membership, members_except_me)
self.assertListEqual(members_except_me, members)
def test_group_creation(self):
self.client.create_group_channel_with.return_value = {
"id": "group-chat-id"}
group_id = match_making.create_group(self.client, ["user-id-0", "user-id-1"])
self.assertEqual(group_id, "group-chat-id")
self.client.create_group_channel_with.assert_called_with(
other_user_ids_list=['user-id-0', 'user-id-1']
)
@mock.patch('coffee_bot.match_making.create_group')
@mock.patch('coffee_bot.match_making.random.shuffle')
def test_making_matches_for_members_even(self, _shuffle, create_group):
members = self._generate_n_members(4)
create_group.side_effect = lambda c, ids: ";".join(ids)
groups_created, left_out = match_making.make_matches_for_members(
client=self.client,
members=members,
group_size=2,
)
self.assertIsNone(left_out, "No one should be left out in this setup")
self.assertListEqual(["user-id-0;user-id-1", "user-id-2;user-id-3"], groups_created)
@mock.patch('coffee_bot.match_making.create_group')
@mock.patch('coffee_bot.match_making.random.shuffle')
def test_making_matches_for_members_odd(self, _shuffle, create_group):
members = self._generate_n_members(5)
create_group.side_effect = lambda c, ids: ";".join(ids)
groups_created, left_out = match_making.make_matches_for_members(
client=self.client,
members=members,
group_size=2,
)
self.assertEqual(left_out, "user-id-4")
self.assertListEqual(["user-id-0;user-id-1", "user-id-2;user-id-3"], groups_created)
@mock.patch('coffee_bot.match_making.make_matches_for_members')
@mock.patch('coffee_bot.match_making.members_in_channel_besides_me')
class MatchingInChannelCase(MatchMakingCaseBase):
def test_minimum_members_with_message(self, members_besides_me, make_matches_for_members):
members_besides_me.return_value = self._generate_n_members(2)
match_making.matching_in_channel(
self.client,
channel_id="channel-id",
too_few_message="Too few",
minimum_members=3,
)
self.client.create_post.assert_called_with(channel_id="channel-id", message="Too few")
make_matches_for_members.assert_not_called()
def test_minimum_members_without_message(self, members_besides_me, make_matches_for_members):
members_besides_me.return_value = self._generate_n_members(2)
match_making.matching_in_channel(
self.client,
channel_id="channel-id",
too_few_message=None,
minimum_members=3,
)
self.client.create_post.assert_not_called()
make_matches_for_members.assert_not_called()
def test_group_hail_with_message(self, members_besides_me, make_matches_for_members):
make_matches_for_members.return_value = (["group-id-1", "group-id-2"], None)
members_besides_me.return_value = self._generate_n_members(2)
match_making.matching_in_channel(
self.client,
channel_id="channel-id",
group_hail="Hail Group!",
minimum_members=0,
)
self.client.create_post.assert_has_calls([
mock.call(channel_id="group-id-1", message="Hail Group!"),
mock.call(channel_id="group-id-2", message="Hail Group!"),
])
def test_group_hail_without_message(self, members_besides_me, make_matches_for_members):
make_matches_for_members.return_value = (["group-id-1", "group-id-2"], None)
members_besides_me.return_value = self._generate_n_members(2)
match_making.matching_in_channel(
self.client,
channel_id="channel-id",
group_hail=None,
minimum_members=0,
)
self.client.create_post.assert_not_called()
def test_left_out_with_message(self, members_besides_me, make_matches_for_members):
make_matches_for_members.return_value = ([], "user-id-0")
self.client.get_user.return_value = {'id': "user-id-0", 'username': "UserId0",
"first_name": "Firstname", "last_name": "LASTNAME"}
members_besides_me.return_value = self._generate_n_members(1)
match_making.matching_in_channel(
self.client,
channel_id="channel-id",
left_out_message="Couldn't match {user[first_name]} {user[last_name]} this time",
minimum_members=0,
)
self.client.create_post.assert_called_with(
channel_id="channel-id",
message="Couldn't match Firstname LASTNAME this time",
)
def test_left_out_without_message(self, members_besides_me, make_matches_for_members):
make_matches_for_members.return_value = ([], "user-id-0")
members_besides_me.return_value = self._generate_n_members(2)
match_making.matching_in_channel(
self.client,
channel_id="channel-id",
left_out_message=None,
minimum_members=0,
)
self.client.get_user.assert_not_called()
self.client.create_post.assert_not_called()
# pylint: disable=missing-class-docstring, missing-function-docstring # pylint: disable=missing-class-docstring, missing-function-docstring
import unittest import unittest
from coffee_bot import coffee_bot from coffee_bot import match_making
class RandomNonrepeatingTuplesCase(unittest.TestCase): class RandomNonrepeatingTuplesCase(unittest.TestCase):
...@@ -8,7 +8,7 @@ class RandomNonrepeatingTuplesCase(unittest.TestCase): ...@@ -8,7 +8,7 @@ class RandomNonrepeatingTuplesCase(unittest.TestCase):
population = ["alma", "korte", "cseresznye", "meggy"] population = ["alma", "korte", "cseresznye", "meggy"]
# pylint: disable=unnecessary-comprehension # pylint: disable=unnecessary-comprehension
random_pairs = [p for p in coffee_bot.random_nonrepeating_tuples(population, 2)] random_pairs = [p for p in match_making.random_nonrepeating_tuples(population, 2)]
self.assertEqual(len(random_pairs), len(population)/2) self.assertEqual(len(random_pairs), len(population)/2)
...@@ -21,7 +21,7 @@ class RandomNonrepeatingTuplesCase(unittest.TestCase): ...@@ -21,7 +21,7 @@ class RandomNonrepeatingTuplesCase(unittest.TestCase):
population = ["alma", "korte", "cseresznye"] population = ["alma", "korte", "cseresznye"]
# pylint: disable=unnecessary-comprehension # pylint: disable=unnecessary-comprehension
random_pairs = [p for p in coffee_bot.random_nonrepeating_tuples(population, 2)] random_pairs = [p for p in match_making.random_nonrepeating_tuples(population, 2)]
self.assertEqual(len(random_pairs), (len(population)+1)/2) self.assertEqual(len(random_pairs), (len(population)+1)/2)
...@@ -48,6 +48,6 @@ class RandomNonrepeatingTuplesCase(unittest.TestCase): ...@@ -48,6 +48,6 @@ class RandomNonrepeatingTuplesCase(unittest.TestCase):
population = [] population = []
# pylint: disable=unnecessary-comprehension # pylint: disable=unnecessary-comprehension
random_pairs = [p for p in coffee_bot.random_nonrepeating_tuples(population, 2)] random_pairs = [p for p in match_making.random_nonrepeating_tuples(population, 2)]
self.assertEqual(len(random_pairs), 0) self.assertEqual(len(random_pairs), 0)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment