diff --git a/sample.config.yaml b/sample.config.yaml index a4ba95b..5ad9e4e 100644 --- a/sample.config.yaml +++ b/sample.config.yaml @@ -41,6 +41,9 @@ vetting: space_id: "!xxx:xxx" # Voting time in seconds voting_time: 172800 + # Requirements for getting accepted + min_yes_votes: 1 + max_no_votes: 0 # Logging setup logging: diff --git a/vetting_bot/config.py b/vetting_bot/config.py index ef8a856..b5c93d3 100644 --- a/vetting_bot/config.py +++ b/vetting_bot/config.py @@ -117,6 +117,13 @@ class Config: self.voting_time = int(self._get_cfg(["vetting", "voting_time"], required=True)) + self.min_yes_votes = int( + self._get_cfg(["vetting", "min_yes_votes"], required=True) + ) + self.max_no_votes = int( + self._get_cfg(["vetting", "max_no_votes"], required=True) + ) + def _get_cfg( self, path: List[str], diff --git a/vetting_bot/timer.py b/vetting_bot/timer.py index 30e8a67..49e4fd0 100644 --- a/vetting_bot/timer.py +++ b/vetting_bot/timer.py @@ -2,8 +2,15 @@ import asyncio import logging import time -from nio import AsyncClient, RoomSendError +from nio import ( + AsyncClient, + RoomMessagesError, + RoomSendError, + RoomSendResponse, + UnknownEvent, +) +from vetting_bot.chat_functions import react_to_event, send_text_to_room from vetting_bot.config import Config from vetting_bot.storage import Storage @@ -48,6 +55,8 @@ class Timer: asyncio.create_task(_task()) async def _end_poll(self, mxid: str, poll_event_id: str): + logger.info("Ending poll for %s - %s", mxid, poll_event_id) + # Send poll end event event_content = { "m.relates_to": { "rel_type": "m.reference", @@ -65,7 +74,108 @@ class Timer: logger.error(poll_resp, stack_info=True) return - self.store.cursor.execute( - "UPDATE vetting SET vote_ended = 1 WHERE mxid = ?", - (mxid,), + # Gather votes + message_filter = { + # "types": ["org.matrix.msc3381.poll.response"], # this doesn't work for some reason :/ + "rooms": [self.config.vetting_room_id], + } + + vote_count = { + "yes": 0, + "no": 0, + "blank": 0, + } + + users_voted = set() + + # Loop until we find all events that could be related to the poll + # (max 20 times: 20 * 20 = up to 400 events deep or until we find the poll event) + start_token = "" + for _ in range(0, 20): + logger.debug("Requesting events") + message_resp = await self.client.room_messages( + room_id=self.config.vetting_room_id, + start=start_token, + limit=20, + message_filter=message_filter, + ) + + if isinstance(message_resp, RoomMessagesError): + logging.error(message_resp, stack_info=True) + text = "Unable to gather votes." + await send_text_to_room(self.client, self.config.vetting_room_id, text) + return + + # Resume next request where this ends + start_token = message_resp.end + + # Count votes + for event in message_resp.chunk: + # Only process poll response events + if not isinstance(event, UnknownEvent): + continue + if event.type != "org.matrix.msc3381.poll.response": + continue + content = event.source.get("content") + try: + # Check if this response is for the correct poll + related_event_id = content["m.relates_to"]["event_id"] + if related_event_id != poll_event_id: + continue + + # Add vote to count + answer = content["org.matrix.msc3381.poll.response"]["answers"][0] + + # Only count the last poll response event + if event.sender in users_voted: + continue + users_voted.add(event.sender) + vote_count[answer] += 1 + except KeyError: + pass + + # Check if we found the initial poll event + if any([event.event_id == poll_event_id for event in message_resp.chunk]): + break + + votes_responses = "".join( + [f"\n{answer.title()}: {count};" for answer, count in vote_count.items()] ) + + # Make the decision by checking requirements + decision = ( + vote_count["yes"] >= self.config.min_yes_votes + and vote_count["no"] <= self.config.max_no_votes + ) + + decision_text = ( + "Confirm inviting this person to the Federation by reacting." + if decision + else "Votes do not match the requirements, not inviting." + ) + + text = ( + f"Voting for `{mxid}` has ended. Counted votes are:\n" + f"{votes_responses}\n\n{decision_text}" + ) + decision_resp = await send_text_to_room( + self.client, self.config.vetting_room_id, text + ) + + if not isinstance(decision_resp, RoomSendResponse): + logger.error(decision_resp) + return + + if decision: + await react_to_event( + self.client, + self.config.vetting_room_id, + decision_resp.event_id, + "confirm", + ) + + # Finally - update database + # self.store.cursor.execute( + # "UPDATE vetting SET vote_ended = 1 WHERE mxid = ?", + # (mxid,), + # )