From 86005cf7927d375c3622e6a3a61e13a6d47e46e6 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 09:51:45 +0000 Subject: [PATCH 01/22] SAVEPOINT --- .devcontainer/Dockerfile | 22 -- .devcontainer/devcontainer.json | 57 ---- README.md | 74 +++-- build-and-run.bat | 4 - commands/__init__.py | 16 -- commands/abstract_command.py | 11 - commands/add_karma.py | 52 ---- commands/add_member.py | 23 -- commands/cocktail_of_the_week_search.py | 46 --- commands/get_current_karma_reasons.py | 18 -- commands/get_current_net_karma.py | 18 -- commands/get_leader_board.py | 17 -- commands/get_loser_board.py | 18 -- commands/giphy_translate.py | 32 --- commands/google_image_search.py | 32 --- commands/google_search.py | 31 -- commands/reg_search.py | 51 ---- commands/release.py | 29 -- commands/urban_search.py | 32 --- commands/void_command.py | 20 -- commands/wikipedia_search.py | 34 --- config.py | 55 ---- logging.conf | 32 --- main.py | 6 - model/__init__.py | 2 - model/karma.py | 109 ------- model/member.py | 39 --- requirements.txt | 52 ---- run.dockerfile | 5 - setup.py | 12 - slack_channel/__init__.py | 18 -- slack_channel/abstract_event_handler.py | 101 ------- slack_channel/abstract_reaction_handler.py | 48 ---- slack_channel/add_karma_event_handler.py | 141 ---------- slack_channel/add_karma_reaction_handler.py | 29 -- .../cocktail_of_the_week_event_handler.py | 34 --- ...get_current_karma_reasons_event_handler.py | 60 ---- .../get_current_net_karma_event_handler.py | 52 ---- .../get_leader_board_event_handler.py | 48 ---- .../get_loser_board_event_handler.py | 48 ---- slack_channel/giphy_event_handler.py | 35 --- slack_channel/google_event_handler.py | 34 --- slack_channel/google_image_event_handler.py | 34 --- slack_channel/help_event_handler.py | 54 ---- slack_channel/initial_memberlist_populator.py | 46 --- slack_channel/reaction_test_handler.py | 29 -- slack_channel/reg_event_handler.py | 35 --- slack_channel/release_event_handler.py | 32 --- slack_channel/slack_event_handler.py | 246 ---------------- slack_channel/slack_message_broker.py | 48 ---- slack_channel/slack_parser.py | 34 --- slack_channel/test_event_handler.py | 30 -- slack_channel/urban_event_handler.py | 41 --- slack_channel/wikipedia_event_handler.py | 35 --- tag.ps1 | 5 - test.dockerfile | 5 - tests/__init__.py | 0 tests/command_tests/__init__.py | 0 tests/command_tests/test_add_karma.py | 41 --- .../test_cocktail_of_the_week_search.py | 20 -- .../test_get_current_karma_reasons.py | 24 -- .../test_get_current_net_karma.py | 23 -- tests/command_tests/test_get_leader_board.py | 44 --- tests/command_tests/test_giphy_translate.py | 20 -- .../command_tests/test_google_image_search.py | 24 -- tests/command_tests/test_google_search.py | 27 -- tests/command_tests/test_reg_search.py | 64 ----- tests/command_tests/test_release_command.py | 12 - tests/command_tests/test_urban_search.py | 33 --- tests/command_tests/test_wikipedia_search.py | 33 --- tests/model_tests/__init__.py | 0 tests/model_tests/test_karma.py | 266 ------------------ tests/model_tests/test_member.py | 46 --- tests/slack_channel_tests/__init__.py | 0 .../test_add_karma_reaction_handler.py | 48 ---- ...test_cocktail_of_the_week_event_handler.py | 66 ----- .../test_decrement_karma_event_handler.py | 72 ----- ...get_current_karma_reasons_event_handler.py | 110 -------- ...est_get_current_net_karma_event_handler.py | 121 -------- .../test_get_leader_board_event_handler.py | 110 -------- .../test_get_loser_board_event_handler.py | 110 -------- .../test_giphy_event_handler.py | 66 ----- .../test_google_event_handler.py | 66 ----- .../test_google_image_event_handler.py | 66 ----- .../test_help_event_handler.py | 63 ----- .../test_increment_karma_event_handler.py | 68 ----- .../test_karma_handler_command_parsing.py | 97 ------- .../test_release_event_handler.py | 66 ----- .../slack_channel_tests/test_slack_parser.py | 49 ---- .../test_urban_event_handler.py | 72 ----- .../test_wikipedia_event_handler.py | 66 ----- tests/test_config.py | 15 - tox.ini | 5 - 93 files changed, 48 insertions(+), 4236 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 build-and-run.bat delete mode 100644 commands/__init__.py delete mode 100644 commands/abstract_command.py delete mode 100644 commands/add_karma.py delete mode 100644 commands/add_member.py delete mode 100644 commands/cocktail_of_the_week_search.py delete mode 100644 commands/get_current_karma_reasons.py delete mode 100644 commands/get_current_net_karma.py delete mode 100644 commands/get_leader_board.py delete mode 100644 commands/get_loser_board.py delete mode 100644 commands/giphy_translate.py delete mode 100644 commands/google_image_search.py delete mode 100644 commands/google_search.py delete mode 100644 commands/reg_search.py delete mode 100644 commands/release.py delete mode 100644 commands/urban_search.py delete mode 100644 commands/void_command.py delete mode 100644 commands/wikipedia_search.py delete mode 100644 config.py delete mode 100644 logging.conf delete mode 100644 main.py delete mode 100644 model/__init__.py delete mode 100644 model/karma.py delete mode 100644 model/member.py delete mode 100644 requirements.txt delete mode 100644 run.dockerfile delete mode 100644 setup.py delete mode 100644 slack_channel/__init__.py delete mode 100644 slack_channel/abstract_event_handler.py delete mode 100644 slack_channel/abstract_reaction_handler.py delete mode 100644 slack_channel/add_karma_event_handler.py delete mode 100644 slack_channel/add_karma_reaction_handler.py delete mode 100644 slack_channel/cocktail_of_the_week_event_handler.py delete mode 100644 slack_channel/get_current_karma_reasons_event_handler.py delete mode 100644 slack_channel/get_current_net_karma_event_handler.py delete mode 100644 slack_channel/get_leader_board_event_handler.py delete mode 100644 slack_channel/get_loser_board_event_handler.py delete mode 100644 slack_channel/giphy_event_handler.py delete mode 100644 slack_channel/google_event_handler.py delete mode 100644 slack_channel/google_image_event_handler.py delete mode 100644 slack_channel/help_event_handler.py delete mode 100644 slack_channel/initial_memberlist_populator.py delete mode 100644 slack_channel/reaction_test_handler.py delete mode 100644 slack_channel/reg_event_handler.py delete mode 100644 slack_channel/release_event_handler.py delete mode 100644 slack_channel/slack_event_handler.py delete mode 100644 slack_channel/slack_message_broker.py delete mode 100644 slack_channel/slack_parser.py delete mode 100644 slack_channel/test_event_handler.py delete mode 100644 slack_channel/urban_event_handler.py delete mode 100644 slack_channel/wikipedia_event_handler.py delete mode 100644 tag.ps1 delete mode 100644 test.dockerfile delete mode 100644 tests/__init__.py delete mode 100644 tests/command_tests/__init__.py delete mode 100644 tests/command_tests/test_add_karma.py delete mode 100644 tests/command_tests/test_cocktail_of_the_week_search.py delete mode 100644 tests/command_tests/test_get_current_karma_reasons.py delete mode 100644 tests/command_tests/test_get_current_net_karma.py delete mode 100644 tests/command_tests/test_get_leader_board.py delete mode 100644 tests/command_tests/test_giphy_translate.py delete mode 100644 tests/command_tests/test_google_image_search.py delete mode 100644 tests/command_tests/test_google_search.py delete mode 100644 tests/command_tests/test_reg_search.py delete mode 100644 tests/command_tests/test_release_command.py delete mode 100644 tests/command_tests/test_urban_search.py delete mode 100644 tests/command_tests/test_wikipedia_search.py delete mode 100644 tests/model_tests/__init__.py delete mode 100644 tests/model_tests/test_karma.py delete mode 100644 tests/model_tests/test_member.py delete mode 100644 tests/slack_channel_tests/__init__.py delete mode 100644 tests/slack_channel_tests/test_add_karma_reaction_handler.py delete mode 100644 tests/slack_channel_tests/test_cocktail_of_the_week_event_handler.py delete mode 100644 tests/slack_channel_tests/test_decrement_karma_event_handler.py delete mode 100644 tests/slack_channel_tests/test_get_current_karma_reasons_event_handler.py delete mode 100644 tests/slack_channel_tests/test_get_current_net_karma_event_handler.py delete mode 100644 tests/slack_channel_tests/test_get_leader_board_event_handler.py delete mode 100644 tests/slack_channel_tests/test_get_loser_board_event_handler.py delete mode 100644 tests/slack_channel_tests/test_giphy_event_handler.py delete mode 100644 tests/slack_channel_tests/test_google_event_handler.py delete mode 100644 tests/slack_channel_tests/test_google_image_event_handler.py delete mode 100644 tests/slack_channel_tests/test_help_event_handler.py delete mode 100644 tests/slack_channel_tests/test_increment_karma_event_handler.py delete mode 100644 tests/slack_channel_tests/test_karma_handler_command_parsing.py delete mode 100644 tests/slack_channel_tests/test_release_event_handler.py delete mode 100644 tests/slack_channel_tests/test_slack_parser.py delete mode 100644 tests/slack_channel_tests/test_urban_event_handler.py delete mode 100644 tests/slack_channel_tests/test_wikipedia_event_handler.py delete mode 100644 tests/test_config.py delete mode 100644 tox.ini diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index bac91b1..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/python-3/.devcontainer/base.Dockerfile - -# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 -ARG VARIANT="3" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi - -# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. -# COPY requirements.txt /tmp/pip-tmp/ -# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ -# && rm -rf /tmp/pip-tmp - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment this line to install global node packages. -# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 9167918..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,57 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/python-3 -{ - "name": "Python 3", - "build": { - "dockerfile": "Dockerfile", - "context": "..", - "args": { - // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 - "VARIANT": "3", - // Options - "INSTALL_NODE": "false", - "NODE_VERSION": "lts/*", - "--env-file": "${localWorkspaceFolder}/.env" - } - }, - - "runArgs": ["--env-file", "${localWorkspaceFolder}/.env"], - - // Set *default* container specific settings.json values on container create. - "customizations": { - "vscode": { - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/local/bin/python", - "python.languageServer": "Pylance", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" - }, - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-azuretools.vscode-docker", - "eamodio.gitlens", - "kuscamara.yamllint-fix" - ] - } - }, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "pip3 install --user -r requirements.txt", - - // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" -} diff --git a/README.md b/README.md index 6b77970..ff3a4be 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,83 @@ # bottomly -[![Build & Test](https://github.com/Sharkwald/bottomly/actions/workflows/python-app.yml/badge.svg)](https://github.com/Sharkwald/bottomly/actions/workflows/python-app.yml) +[![Build & Test](https://github.com/Sharkwald/bottomly/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Sharkwald/bottomly/actions/workflows/dotnet.yml) -A python slack bot +A .NET Slack bot ## Running Bottomly ### Development -Simply executing `main.py` will allow Bottomly to run, connect to slack, and listen for events. +The project uses [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) for local orchestration. Running the `Bottomly.AppHost` project will spin up MongoDB, Ollama (LLM), and the bot itself: + +```bash +cd bottomly.net +dotnet run --project Bottomly.AppHost +``` ### Production -The `dockerfile` allows the easy creation of a simple docker image based on Alpine which will allow the app to be run from within a container and hosted in your container framework of choice. +Run the `Bottomly` project directly, ensuring MongoDB and Ollama are available and all required environment variables are set: + +```bash +cd bottomly.net +dotnet run --project Bottomly +``` ## Design -Modules are broadly divided by responsibility. Files in the root are related to global config and application spin up, everyhing else is in a module. +The solution is divided into four projects: + +* **Bottomly** — the main application +* **Bottomly.AppHost** — .NET Aspire orchestration host for local development +* **Bottomly.ServiceDefaults** — shared Aspire configuration (telemetry, service discovery, HTTP resilience) +* **Bottomly.Tests** — xUnit test suite + +Within the `Bottomly` project, code is organised by responsibility: ### Commands -Classes in here implement the actions sent to the bot via the delivery channel(s). In most cases they will peform some action and return a result directly, without calling out to other modules, however, if access to the persistence layer is required, it should be done through the `model` module. +Classes here implement the actions triggered by bot commands. Each command performs an action and returns a result, delegating any persistence work to the `Repositories` layer. -### Model +### Repositories -This defines the persistence layer of the bot. All behaviours which relate to persisting information beyond a simple request/response model should be defined here, e.g. describing user histories, their karma, different user lists etc. +Defines the persistence layer. All behaviours which relate to persisting information beyond a simple request/response cycle are defined here, e.g. user karma and member lists. Backed by MongoDB. -### Slack_channel +### Slack -Implements the slack delivery channel, which is currently the only delivery channel. In future, the potential exists to define other ones, such as IRC, without needing to re-implement the `command` or `model` modules. +Implements the Slack delivery channel using [SlackNet](https://github.com/soxtoby/SlackNet) in Socket Mode. -Uses the `SlackSocket` implementation of Slack's RTM protocl. +`SlackWorker` is a background service that maintains the Slack connection and dispatches incoming events. Event handlers are split by type: -`slack_event_handler.py` handles the broad event loop, and loops through the custom handlers to find one that can handle the given message event. +* `MessageEventHandlers/` — handle text messages. Each handler implements `IMessageEventHandler`, which requires: + * `CanHandle(message)`: returns `true` if this handler should process the given message. + * `Handle(message)`: extracts relevant information and invokes the corresponding command. +* `ReactionHandlers/` — handle emoji reactions (e.g. karma changes). +* `MembershipEventHandlers/` — handle member join events. -`*_event_handler.py` files implement custom handlers and implement `AbstractEventHandler`. This mandates that they implement the following functions: +### LlmBot -* `can_handle(slack_event)`: registers that this handler for a particular slack event. Returns a boolean. -* `handle(slack_event)`: extracts the relevant information from the given `slack_event` and passes that to a corresponding `command` object for execution. -* `_get_command_symbol`: Returns the trigger character(s) which specify what messages this handler will call on, e.g. `.g` for a google search command (assuming the bot is configured to use `.` as a common prefix). +Wraps [OllamaSharp](https://github.com/awaescher/OllamaSharp) to provide conversational AI responses via a locally-hosted Ollama instance. ### Tests -Has an internal structure matching the rest of the app, and each app file should have a corresponding test file. +Has an internal structure matching the rest of the app. Each app file should have a corresponding test file. ## Configuration -The following environment variables _must_ be configured for the app to run & all unit tests pass: +The following secrets/environment variables _must_ be configured for the app to run: + +* `bottomly_env`: Describes the active environment. If not set to `live`, output messages will be marked as "DEBUG". +* `bottomly_prefix`: The prefix for bot commands (e.g. `!`). Most commands require this prefix, allowing easy switching between test and production environments. +* `bottomly_slack_bot_token`: The Slack bot token (`xoxb-...`) for Socket Mode access. +* `bottomly_slack_app_token`: The Slack app-level token (`xapp-...`) for Socket Mode. +* `bottomly_google_api_key`: A valid Google API key. +* `bottomly_google_cse_id`: A valid Google Custom Search Engine ID. +* `bottomly_giphy_api_key`: A valid Giphy API key. +* `bottomly_github_token`: A GitHub personal access token. -* `bottomly_env`: A string describing the active environment. If it is not set to `live` all output messages will display marked as "DEBUG". -* `bottomly_prefix`: The prefix for bottomly commands. While events can overload this, for example karma changes can trigger on `++` or `--` regardless of the common prefix, most commands should require a single character prefix such as `.` or `!`. This sets that prefix globally, allowing for easy switching between test and production, for example. -* `bottomly_google_api_key`: A valid google API key -* `bottomly_google_cse_id`: A valid google custom search engine ID -* `bottomly_mongo_conn_str`: A mongo DB connection string. Since we're using `pymodm` for data access, this connection string must include a DB name. -* `bottomly_slack_bot_token`: The "Bot User OAuth Access Token" from slack to allow access to their RTM endpoint. -* `bottomly_giphy_api_key`: a valid giphy API key. +MongoDB and Ollama connection strings are managed automatically by Aspire in development. In production, configure them via standard .NET connection string settings. ## Contributing -Most actions will be carried out by adding a `handler`/`command` pair in the relevant modules, with the command handing off to the model layer for any work that requires persistence. PRs will *not be merged* without proper test coverage. +Most additions will involve adding a handler and command pair in the relevant modules, with the command delegating to the repository layer for any persistence work. PRs will *not be merged* without proper test coverage. diff --git a/build-and-run.bat b/build-and-run.bat deleted file mode 100644 index 6d1e567..0000000 --- a/build-and-run.bat +++ /dev/null @@ -1,4 +0,0 @@ -docker stop bottomly -docker build --tag bottomly -f run.dockerfile . -docker system prune -f -docker run --rm --name bottomly --env-file .env bottomly \ No newline at end of file diff --git a/commands/__init__.py b/commands/__init__.py deleted file mode 100644 index 72c6bc9..0000000 --- a/commands/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# coding=utf-8 -from .abstract_command import AbstractCommand -from .google_search import GoogleSearchCommand -from .urban_search import UrbanSearchCommand -from .reg_search import RegSearchCommand -from .wikipedia_search import WikipediaSearchCommand -from .add_karma import AddKarmaCommand -from .get_current_net_karma import GetCurrentNetKarmaCommand -from .get_current_karma_reasons import GetCurrentKarmaReasonsCommand -from .giphy_translate import GiphyTranslateCommand -from .get_leader_board import GetLeaderBoardCommand -from .get_loser_board import GetLoserBoardCommand -from .void_command import VoidCommand -from .google_image_search import GoogleImageSearchCommand -from .cocktail_of_the_week_search import CocktailOfTheWeekSearchCommand -from .release import ReleaseCommand diff --git a/commands/abstract_command.py b/commands/abstract_command.py deleted file mode 100644 index ee19479..0000000 --- a/commands/abstract_command.py +++ /dev/null @@ -1,11 +0,0 @@ -from abc import ABC, abstractmethod - -class AbstractCommand(ABC): - - @abstractmethod - def execute(self, **kwargs): - pass - - @abstractmethod - def get_purpose(self): - pass \ No newline at end of file diff --git a/commands/add_karma.py b/commands/add_karma.py deleted file mode 100644 index 701cec1..0000000 --- a/commands/add_karma.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding=utf-8 -from datetime import datetime - -from commands.abstract_command import AbstractCommand -from config import Config -from model.karma import Karma, KarmaType - - -class AddKarmaCommand(AbstractCommand): - - def get_purpose(self): - return "Awards an imaginary internet point to someone/something." - - def execute(self, awarded_to, awarded_by, reason, karma_type): - self.config.connect_to_db() - k = Karma(awarded_to_username=awarded_to, - reason=reason, - awarded_by_username=awarded_by, - awarded=datetime.now(), - karma_type=str(karma_type)) - - k.save() - return k - - @staticmethod - def get_karma_reactions() -> dict: - return { - "+1": KarmaType.POZZYPOZ, - "arrow_up": KarmaType.POZZYPOZ, - "clap": KarmaType.POZZYPOZ, - "heart": KarmaType.POZZYPOZ, - "heart_eyes": KarmaType.POZZYPOZ, - "heavy_plus_sign": KarmaType.POZZYPOZ, - "heavy_tick": KarmaType.POZZYPOZ, - "joy": KarmaType.POZZYPOZ, - "party_parrot": KarmaType.POZZYPOZ, - "raised_hands": KarmaType.POZZYPOZ, - "smile": KarmaType.POZZYPOZ, - "thumbsup": KarmaType.POZZYPOZ, - "-1": KarmaType.NEGGYNEG, - "arrow_down": KarmaType.NEGGYNEG, - "hankey": KarmaType.NEGGYNEG, - "heavy_minus_sign": KarmaType.NEGGYNEG, - "poo": KarmaType.NEGGYNEG, - "poop": KarmaType.NEGGYNEG, - "shit": KarmaType.NEGGYNEG, - "thumbsdown": KarmaType.NEGGYNEG - } - - def __init__(self): - super(AddKarmaCommand, self) - self.config = Config() diff --git a/commands/add_member.py b/commands/add_member.py deleted file mode 100644 index fe93ed2..0000000 --- a/commands/add_member.py +++ /dev/null @@ -1,23 +0,0 @@ -from commands.abstract_command import AbstractCommand -from model.member import Member - - -class AddMemberCommand(AbstractCommand): - - def get_purpose(self): - return "Adds a new chat member to the persistent storage." - - def execute(self): - # Check if username is unique, otherwise barf - member_exists = len(list(Member.objects.raw({'_id': self.username}))) != 0 - if member_exists: - return - - # instantiate & save new member - m = Member(username=self.username, slack_id=self.slack_id) - m.save() - - def __init__(self, username, slack_id): - super(AddMemberCommand, self) - self.username = username - self.slack_id = slack_id diff --git a/commands/cocktail_of_the_week_search.py b/commands/cocktail_of_the_week_search.py deleted file mode 100644 index 74db304..0000000 --- a/commands/cocktail_of_the_week_search.py +++ /dev/null @@ -1,46 +0,0 @@ -from commands.abstract_command import AbstractCommand - -import os -import requests -from bs4 import BeautifulSoup - - -class CocktailOfTheWeekSearchCommand(AbstractCommand): - - def get_purpose(self): - return 'Cocktail of the week, a reminder we were once fun.' - - def execute(self): - base_url = 'https://thebrassmonkeygla.co.uk/' - - response = requests.get(base_url) - soup = BeautifulSoup(response.content, 'html.parser') - - try: - # Header Row - Used to identify - header_row = soup.find(text='Cocktail of the Week').parent.parent.parent - - # Blank Row - Because fuck restaurant websites - blank_row = header_row.find_next('tr') - - # Name & Price Row - name_row = blank_row.find_next('tr') - name_row_data_cells = name_row.findAll('td') - name = name_row_data_cells[0].text - price = name_row_data_cells[1].text - - # Ingredients Row - ingredients_row = name_row.find_next('tr') - ingredients = ingredients_row.find('td').text - - return 'Cocktail of the Week' + os.linesep + name + ' - (' + price + ')' + os.linesep + ingredients - - except: - error_element = soup.find('div', {'class': 'ErrorMessage'}) - error_message = error_element.findChild('h3').text - - return error_message - - -def __init__(self): - super(CocktailOfTheWeekSearchCommand, self) diff --git a/commands/get_current_karma_reasons.py b/commands/get_current_karma_reasons.py deleted file mode 100644 index 0a14c20..0000000 --- a/commands/get_current_karma_reasons.py +++ /dev/null @@ -1,18 +0,0 @@ -from commands.abstract_command import AbstractCommand -from config import Config -from model.karma import Karma - - -class GetCurrentKarmaReasonsCommand(AbstractCommand): - - def get_purpose(self): - return "Returns the justifications for someone's/something's current score of imaginary internet points" - - def execute(self, recipient): - reasons = Karma.get_current_karma_reasons_for_recipient(recipient) - return reasons - - def __init__(self): - super(GetCurrentKarmaReasonsCommand, self) - config = Config() - config.connect_to_db() \ No newline at end of file diff --git a/commands/get_current_net_karma.py b/commands/get_current_net_karma.py deleted file mode 100644 index 254adc4..0000000 --- a/commands/get_current_net_karma.py +++ /dev/null @@ -1,18 +0,0 @@ -from commands.abstract_command import AbstractCommand -from config import Config -from model.karma import Karma - - -class GetCurrentNetKarmaCommand(AbstractCommand): - - def get_purpose(self): - return "Returns someone's/something's current score of imaginary internet points" - - def execute(self, recipient): - net_karma = Karma.get_current_net_karma_for_recipient(recipient) - return net_karma - - def __init__(self): - super(GetCurrentNetKarmaCommand, self) - config = Config() - config.connect_to_db() \ No newline at end of file diff --git a/commands/get_leader_board.py b/commands/get_leader_board.py deleted file mode 100644 index b0371de..0000000 --- a/commands/get_leader_board.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding=utf-8 -from commands.abstract_command import AbstractCommand -from config import Config -from model.karma import Karma - - -class GetLeaderBoardCommand(AbstractCommand): - def get_purpose(self): - return "Shows the best of the best!" - - def execute(self, size: int=3): - return Karma.get_leader_board(size=size) - - def __init__(self): - super(GetLeaderBoardCommand, self) - config = Config() - config.connect_to_db() diff --git a/commands/get_loser_board.py b/commands/get_loser_board.py deleted file mode 100644 index 6df022c..0000000 --- a/commands/get_loser_board.py +++ /dev/null @@ -1,18 +0,0 @@ -from commands.abstract_command import AbstractCommand -from config import Config -from model.karma import Karma - - -class GetLoserBoardCommand(AbstractCommand): - - def get_purpose(self): - return "Gets the current karma loserboard" - - def execute(self, size: int=3): - reasons = Karma.get_loser_board(size=size) - return reasons - - def __init__(self): - super(GetLoserBoardCommand, self) - config = Config() - config.connect_to_db() \ No newline at end of file diff --git a/commands/giphy_translate.py b/commands/giphy_translate.py deleted file mode 100644 index 641d2d2..0000000 --- a/commands/giphy_translate.py +++ /dev/null @@ -1,32 +0,0 @@ -import requests - -from commands.abstract_command import AbstractCommand -from config import Config, ConfigKeys - - -def _result_set_is_empty(results): - return len(results["data"]) == 0 - - -class GiphyTranslateCommand(AbstractCommand): - def get_purpose(self): - return "Uses Giphy to find a gif matching the given search term" - - def execute(self, search_term): - if search_term is None or search_term == '': - return None - - base_url = "http://api.giphy.com/v1/gifs/translate?limit=1&api_key=" + self.giphy_key + "&s=" - response = requests.get(base_url + search_term) - results = response.json() - - if _result_set_is_empty(results): - return None - - gif_url = results["data"]["url"] - return gif_url - - def __init__(self): - c = Config() - self.giphy_key = c.get_config_value(ConfigKeys.giphy_api_key) - super(GiphyTranslateCommand, self) diff --git a/commands/google_image_search.py b/commands/google_image_search.py deleted file mode 100644 index 5fa063a..0000000 --- a/commands/google_image_search.py +++ /dev/null @@ -1,32 +0,0 @@ -import warnings - -from commands.abstract_command import AbstractCommand -from config import ConfigKeys, Config -from googleapiclient.discovery import build - - -class GoogleImageSearchCommand(AbstractCommand): - - def get_purpose(self): - return "Performs a google image search and returns the top hit." - - def execute(self, search_term): - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - if search_term is None or search_term == '': - return None - service = build("customsearch", "v1", developerKey=self.api_key) - results = service.cse().list(q=search_term, cx=self.cse_id, num=1, searchType='image').execute() - if not 'items' in results: - return None - else: - return results['items'][0]['link'] - - def _result_set_is_empty(self, results): - return results['searchInformation']['totalResults'] == '0' - - def __init__(self): - super(GoogleImageSearchCommand, self) - config = Config() - self.api_key = config.get_config_value(ConfigKeys.google_api_key) - self.cse_id = config.get_config_value(ConfigKeys.google_cse_id) diff --git a/commands/google_search.py b/commands/google_search.py deleted file mode 100644 index bb4ad73..0000000 --- a/commands/google_search.py +++ /dev/null @@ -1,31 +0,0 @@ -import warnings - -from commands.abstract_command import AbstractCommand -from config import ConfigKeys, Config -from googleapiclient.discovery import build - - -class GoogleSearchCommand(AbstractCommand): - - def get_purpose(self): - return "Performs a google search and returns the top hit." - - def execute(self, search_term): - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - if search_term is None or search_term == '': - return None - service = build("customsearch", "v1", developerKey=self.api_key) - results = service.cse().list(q=search_term, cx=self.cse_id, num=1).execute() - if self._result_set_is_empty(results): - return None - return results['items'][0] - - def _result_set_is_empty(self, results): - return results['searchInformation']['totalResults'] == '0' - - def __init__(self): - super(GoogleSearchCommand, self) - config = Config() - self.api_key = config.get_config_value(ConfigKeys.google_api_key) - self.cse_id = config.get_config_value(ConfigKeys.google_cse_id) diff --git a/commands/reg_search.py b/commands/reg_search.py deleted file mode 100644 index 3e682e0..0000000 --- a/commands/reg_search.py +++ /dev/null @@ -1,51 +0,0 @@ -from commands.abstract_command import AbstractCommand - -import requests -from bs4 import BeautifulSoup - - -class RegSearchCommand(AbstractCommand): - - def get_purpose(self): - return 'AutoTrader reg lookup, because Jamie is lazy.' - - def execute(self, search_term): - if search_term is None or search_term == '': - return 'Registration missing' - - search_term = search_term.replace(' ', '') - search_term = search_term.lower() - - if len(search_term) > 7: - return 'Registration too long.' - - if not search_term.isalnum(): - return 'Registration should not contain special characters' - - # Sanity checks done, now replacing common mistakes - search_term = search_term.replace('i', '1') - - base_url = 'https://www.vehiclecheck.co.uk/?vrm=' - target_url = base_url + search_term - - r = requests.get(target_url) - soup = BeautifulSoup(r.content, 'html.parser') - - try: - vehicle_colour = soup.find(id='VehicleColour')['value'].strip() - vehicle_year = soup.find(id='RegistrationYear')['value'].strip() - vehicle_make = soup.find(id='VehicleMake')['value'].strip().capitalize() - vehicle_model = soup.find(id='VehicleModel')['value'].strip() - vehicle_image = 'https://www.vehiclecheck.co.uk' + soup.find(id='searchResultCarImage')['src'].strip() - - return vehicle_colour + ' ' + vehicle_make + ' ' + vehicle_model + ' (' + vehicle_year + ')' + ' ' + vehicle_image - - except: - error_element = soup.find('div', {'class': 'ErrorMessage'}) - error_message = error_element.findChild('h3').text - - return error_message - - -def __init__(self): - super(RegSearchCommand, self) diff --git a/commands/release.py b/commands/release.py deleted file mode 100644 index c3013aa..0000000 --- a/commands/release.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging -from github import Github - -from config import ConfigKeys, Config -from commands.abstract_command import AbstractCommand - - -class ReleaseCommand(AbstractCommand): - def get_purpose(self): - return 'Describes the latest release of bottomly' - - def execute(self): - try: - g = Github(self.token) - repo = g.get_user().get_repo('bottomly') - release = repo.get_latest_release() - release_desc = f'Latest Release: *{release.title}* v{release.tag_name}' - release_desc += f'\n_Published at {release.published_at}_' - if len(release.body) > 0: - release_desc += f'\n{release.body}' - return release_desc - except Exception as ex: - logging.exception('Error retrieving release info from Github: ' + str(ex)) - return None - - def __init__(self): - super(ReleaseCommand, self) - config = Config() - self.token = config.get_config_value(ConfigKeys.github_token) diff --git a/commands/urban_search.py b/commands/urban_search.py deleted file mode 100644 index 8afb56e..0000000 --- a/commands/urban_search.py +++ /dev/null @@ -1,32 +0,0 @@ -import requests -import random - -from commands.abstract_command import AbstractCommand - - -class UrbanSearchCommand(AbstractCommand): - def get_purpose(self): - return "Tells you what something _really_ means." - - def execute(self, search_term): - if search_term is None or search_term == '': - return None - - base_url = "http://api.urbandictionary.com/v0/define?term=" - results = requests.get(base_url + search_term).json() - - if self._result_set_is_empty(results): - return None - - return self._get_random_result(results) - - def _result_set_is_empty(self, results): - return results['list'] == [] - - def _get_random_result(self, results): - list_total = len(results['list']) - random_index = random.randint(0, list_total-1) - return results['list'][random_index]['definition'] - - def __init__(self): - super(UrbanSearchCommand, self) diff --git a/commands/void_command.py b/commands/void_command.py deleted file mode 100644 index 7ac12d2..0000000 --- a/commands/void_command.py +++ /dev/null @@ -1,20 +0,0 @@ -from commands.abstract_command import AbstractCommand - - -class VoidCommand(AbstractCommand): - """ - An empty command that invokes as little logic as possible. - This is intended to be used by test handlers, purely to ensure - program execution gets down to the command layer. - NB: This could potentially change in future if a use case around - a handler which executes a void command could be described. - """ - - def get_purpose(self): - return "Tests execution flow as far as the command layer." - - def execute(self): - return "" - - def __init__(self): - super(VoidCommand, self) diff --git a/commands/wikipedia_search.py b/commands/wikipedia_search.py deleted file mode 100644 index 501cdae..0000000 --- a/commands/wikipedia_search.py +++ /dev/null @@ -1,34 +0,0 @@ -import requests -import json - -from commands.abstract_command import AbstractCommand - - -class WikipediaSearchCommand(AbstractCommand): - def get_purpose(self): - return "Performs a wikipedia search and returns the top hit." - - def execute(self, search_term): - if search_term is None or search_term == '': - return None - - base_url = "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=" - response = requests.get(base_url + search_term) - results = response.json() - - if self._result_set_is_empty(results[1]): - return None - - top_hit = results[1][0] - link_to_top_hit = results[3][0] - - result = {"link": link_to_top_hit, "text": top_hit} - - return result - - def _result_set_is_empty(self, results): - return len(results) == 0 - - - def __init__(self): - super(WikipediaSearchCommand, self) diff --git a/config.py b/config.py deleted file mode 100644 index 90e6643..0000000 --- a/config.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -from enum import Enum -from pymodm import connect - - -class ConfigKeys(Enum): - google_api_key = "bottomly_google_api_key" - google_cse_id = "bottomly_google_cse_id" - mongo_conn_st = "bottomly_mongo_conn_str" - slack_bot_token = "bottomly_slack_bot_token" - slack_app_token = "bottomly_slack_app_token" - prefix = "bottomly_prefix" - giphy_api_key = "bottomly_giphy_api_key" - env_key = "bottomly_env" - github_token = "bottomly_github_token" - - -class Config(object): - - # Constants - Connection = "bottomly" - _key_err_messages = { - ConfigKeys.google_api_key: 'Google API key is not configured', - ConfigKeys.google_cse_id: 'Google custom search engine ID is not configured', - ConfigKeys.mongo_conn_st: 'MongoDB connection string is not configured', - ConfigKeys.slack_bot_token: 'Slack bot auth token is not configured', - ConfigKeys.slack_app_token: 'Slack app auth token is not configured', - ConfigKeys.prefix: "Standard command prefix is not configured", - ConfigKeys.giphy_api_key: "Giphy API key is not configured", - ConfigKeys.env_key: "Environment mode is not configured", - ConfigKeys.github_token: "Github token is not configured" - } - - # Functions - @staticmethod - def get_config_value(key): - if key.value in os.environ: - return os.environ.get(key.value) - else: - raise EnvironmentError(Config._key_err_messages[key]) - - def connect_to_db(self): - conn_str = self.get_config_value(ConfigKeys.mongo_conn_st) - connect(conn_str, alias=Config.Connection) - - @staticmethod - def get_prefix(): - key = ConfigKeys.prefix - if key.value in os.environ: - return os.environ.get(key.value) - else: - raise EnvironmentError(Config._key_err_messages[key]) - - def __init__(self): - super(Config, self).__init__() diff --git a/logging.conf b/logging.conf deleted file mode 100644 index ec3f764..0000000 --- a/logging.conf +++ /dev/null @@ -1,32 +0,0 @@ -[loggers] -keys=root,bottomly - -[handlers] -keys=bottomlyHandler,bottomlyConsoleHandler - -[formatters] -keys=simpleFormatter - -[logger_root] -level=DEBUG -handlers=bottomlyHandler,bottomlyConsoleHandler - -[logger_bottomly] -level=DEBUG -handlers=bottomlyHandler,bottomlyConsoleHandler -qualname=bottomly -propagate=0 - -[handler_bottomlyHandler] -class=FileHandler -formatter=simpleFormatter -args=('bottomly.log', 'a') - -[handler_bottomlyConsoleHandler] -class=StreamHandler -formatter=simpleFormatter -args=(sys.stdout,) - -[formatter_simpleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt= \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index d02bf95..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -# coding=utf-8 -from config import Config, ConfigKeys -import slack_channel.slack_event_handler - -if __name__ == '__main__': - slack_channel.slack_event_handler.run_slack_socket_mode_client() diff --git a/model/__init__.py b/model/__init__.py deleted file mode 100644 index b39672b..0000000 --- a/model/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -import model.karma -import model.member diff --git a/model/karma.py b/model/karma.py deleted file mode 100644 index 780cb3f..0000000 --- a/model/karma.py +++ /dev/null @@ -1,109 +0,0 @@ -from datetime import datetime, timedelta -from pymodm import MongoModel, fields -from enum import Enum - -from pymodm.errors import ValidationError -from pymongo import WriteConcern -from config import Config - - -karma_expiry_days = 30 - - -class KarmaType(Enum): - POZZYPOZ = 1 - NEGGYNEG = -1 - - -class Karma(MongoModel): - - @staticmethod - def _get_current_net_karma(**kwargs) -> list: - # aggregate defaults - projection = {"$project": {"_id": "$_id", "recipient": {"$toLower": "$awarded_to_username"}, - "net_karma": {"$cond": [{"$eq": ["$karma_type", str(KarmaType.POZZYPOZ)]}, 1, -1]}, - "awarded": "$awarded"}} - match = {"$match": {'awarded': {'$gt': _get_cut_off_date()}}} - grouping = {"$group": {"_id": "$recipient", "net_karma": {"$sum": "$net_karma"}}} - sort = {"$sort": {"net_karma": -1}} - limit = 3 - - # kwarg aggregate overrides - if "recipient" in kwargs: - match["$match"]["recipient"] = str(kwargs["recipient"]).lower() - if "sort" in kwargs and kwargs["sort"] == "asc": - sort["$sort"]["net_karma"] = 1 - if "limit" in kwargs: - limit = int(kwargs["limit"]) - - # execution - query_set = Karma.objects.aggregate(projection, - match, - grouping, - sort) - result_set = list(query_set)[:limit] - return [{"username": r["_id"], "net_karma": r["net_karma"]} for r in result_set] - - @staticmethod - def get_leader_board(size: int=3) -> list: - return Karma._get_current_net_karma(limit=size) - - @staticmethod - def get_loser_board(size: int=3) -> list: - return Karma._get_current_net_karma(limit=size, sort="asc") - - @staticmethod - def get_current_net_karma_for_recipient(recipient: str) -> int: - net_karma_results = Karma._get_current_net_karma(recipient=recipient) - if len(net_karma_results) == 0: - return 0 - recipient_net_karma = net_karma_results[0] - return recipient_net_karma["net_karma"] - - @staticmethod - def get_current_karma_reasons_for_recipient(recipient: str) -> dict: - projection = {"$project": {"_id": "$_id", - "awarded_to_username": {"$toLower": "$awarded_to_username"}, - "awarded_by_username": {"$toLower": "$awarded_by_username"}, - "karma_type": "$karma_type", - "awarded": "$awarded", - "reason": "$reason" - }} - match = {'$match': {'awarded_to_username': recipient.lower(), - 'awarded': {'$gt': _get_cut_off_date()}}} - query_set = Karma.objects.aggregate(projection, match) - recent_karma = [Karma(awarded_to_username=k["awarded_to_username"], - awarded_by_username=k["awarded_by_username"], - karma_type=k["karma_type"], - awarded=k["awarded"], - reason=k["reason"], - _id=k["_id"]) for k in list(query_set)] - karma_with_reasons = [k for k in recent_karma if k.reason != Karma.default_reason] - karma_without_reasons = [k for k in recent_karma if k.reason == Karma.default_reason] - return {'reasonless': len(karma_without_reasons), 'reasoned': karma_with_reasons} - - awarded_to_username = fields.CharField() - default_reason = "" - awarded_by_username = fields.CharField() - reason = fields.CharField(blank=True) - awarded = fields.DateTimeField() - karma_type = fields.CharField() - - def validate_auto_pozzypoz(self): - valid = True - if self.karma_type == str(KarmaType.POZZYPOZ): - valid = self.awarded_by_username != self.awarded_to_username # can't give yourself positive karma - if not valid: - raise ValidationError("Can't give yourself positive karma") - - def clean(self): - self.validate_auto_pozzypoz() - - - class Meta: - write_concern = WriteConcern(j=True) - connection_alias = Config.Connection - - -def _get_cut_off_date() -> datetime: - return datetime.today() - timedelta(days=karma_expiry_days) diff --git a/model/member.py b/model/member.py deleted file mode 100644 index f34fbdc..0000000 --- a/model/member.py +++ /dev/null @@ -1,39 +0,0 @@ - -from pymodm import MongoModel, fields -from pymongo import WriteConcern -from model.karma import Karma -from config import Config - - -class Member(MongoModel): - username = fields.CharField(primary_key=True) - slack_id = fields.CharField() - - @staticmethod - def get_member_by_username(username): - members = list(Member.objects.raw({'_id': username})) - if (len(members) == 0): - return None - else: - return members[0] - - @staticmethod - def get_member_by_slack_id(slack_id): - members = list(Member.objects.raw({'slack_id': slack_id})) - if (len(members) == 0): - return None - else: - return members[0] - - def _get_recent_karma(self): - return Karma.get_recent_karma_for_recipient(self.username) - - def get_current_karma(self): - return Karma.get_current_net_karma_for_recipient(self.username) - - def get_karma_reasons(self): - return Karma.get_current_karma_reasons_for_recipient(self.username) - - class Meta: - write_concern = WriteConcern(j=True) - connection_alias = Config.Connection diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a0f2faf..0000000 --- a/requirements.txt +++ /dev/null @@ -1,52 +0,0 @@ -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 -aiosignal==1.4.0 -async-timeout==5.0.1 -attrs==25.4.0 -beautifulsoup4==4.14.3 -certifi==2026.2.25 -cffi==2.0.0 -chardet==6.0.0.post1 -charset-normalizer==3.4.4 -cryptography==46.0.5 -dnslib==0.9.26 -dnspython==2.8.0 -frozenlist==1.8.0 -google-api-core==2.30.0 -google-api-python-client==2.190.0 -google-auth==2.48.0 -google-auth-httplib2==0.3.0 -googleapis-common-protos==1.72.0 -httplib2==0.31.2 -idna==3.11 -multidict==6.7.1 -oauth2client==4.1.3 -packaging==26.0 -pip-review==1.3.0 -propcache==0.4.1 -proto-plus==1.27.1 -protobuf==6.33.5 -pyasn1==0.6.2 -pyasn1_modules==0.4.2 -pycparser==3.0 -PyGithub==2.8.1 -PyJWT==2.11.0 -pymodm==0.4.3 -pymongo==3.13.0 -PyNaCl==1.6.2 -pyparsing==3.3.2 -requests==2.32.5 -rsa==4.9.1 -six==1.17.0 -slack_sdk==3.40.1 -slackclient==2.9.4 -slacksocket==1.0.1 -soupsieve==2.8.3 -typing_extensions==4.15.0 -uritemplate==4.2.0 -urllib3==2.6.3 -websocket-client==0.56.0 -wrapt==2.1.1 -yamllint==1.38.0 -yamlfix==1.19.1 -yarl==1.23.0 \ No newline at end of file diff --git a/run.dockerfile b/run.dockerfile deleted file mode 100644 index 3682aa5..0000000 --- a/run.dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM python:3 -COPY . /app -WORKDIR /app -RUN pip install -r requirements.txt -CMD python main.py \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index ff1ead2..0000000 --- a/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from distutils.core import setup - -setup( - name='bottomly', - version='', - packages=[''], - url='', - license='', - author='Owen Morgan-Jones', - author_email='owen.morganjones@gmail.com', - description='' -) diff --git a/slack_channel/__init__.py b/slack_channel/__init__.py deleted file mode 100644 index ac99dd9..0000000 --- a/slack_channel/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# coding=utf-8 -from .urban_event_handler import UrbanEventHandler -from .google_event_handler import GoogleEventHandler -from .reg_event_handler import RegEventHandler -from .wikipedia_event_handler import WikipediaEventHandler -from .add_karma_event_handler import IncrementKarmaEventHandler, DecrementKarmaEventHandler -from .get_current_net_karma_event_handler import GetCurrentNetKarmaEventHandler -from .get_current_karma_reasons_event_handler import GetCurrentKarmaReasonsEventHandler -from .help_event_handler import HelpEventHandler -from .giphy_event_handler import GiphyEventHandler -from .get_leader_board_event_handler import GetLeaderBoardEventHandler -from .get_loser_board_event_handler import GetLoserBoardEventHandler -from .test_event_handler import TestEventHandler -from .reaction_test_handler import ReactionTestHandler -from .google_image_event_handler import GoogleImageEventHandler -from .add_karma_reaction_handler import AddKarmaReactionHandler -from .cocktail_of_the_week_event_handler import CocktailOfTheWeekEventHandler -from .release_event_handler import ReleaseEventHandler diff --git a/slack_channel/abstract_event_handler.py b/slack_channel/abstract_event_handler.py deleted file mode 100644 index 7cb17aa..0000000 --- a/slack_channel/abstract_event_handler.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -import logging.config -import os -from os import path -from abc import ABC, abstractmethod - -from commands.abstract_command import AbstractCommand -from config import Config, ConfigKeys -from slack_channel.slack_message_broker import SlackMessageBroker - -help_token = "-?" - -log_file_path = path.join(path.dirname(path.abspath(__file__)), '../logging.conf') -logging.config.fileConfig(log_file_path) -logger = logging.getLogger('bottomly') - -class AbstractEventHandler(ABC): - - @property - @abstractmethod - def command(self) -> AbstractCommand: - pass - - @property - @abstractmethod - def name(self) -> str: - pass - - @abstractmethod - def can_handle(self, slack_event) -> bool: - pass - - @abstractmethod - def _invoke_handler_logic(self, slack_event): - pass - - @abstractmethod - def _get_command_symbol(self) -> str: - pass - - @abstractmethod - def get_usage(self) -> str: - pass - - def get_usage_addendum(self) -> str: - return "" - - @property - def _help_message(self): - return self.command_trigger + help_token - - def handle(self, slack_event): - logger.info(str(self) + " Handling: " + str(slack_event)) - try: - if self._is_help_event(slack_event): - self._handle_help_event(slack_event) - else: - self._invoke_handler_logic(slack_event) - except Exception: - logger.exception("Error thrown handling event: " + slack_event) - - def _is_help_event(self, slack_event): - is_help_event = slack_event["text"] == self._help_message - return is_help_event - - def _handle_help_event(self, slack_event): - message = self.build_help_message() - self._send_message_response(message, slack_event) - - def build_help_message(self) -> str: - name = self.name + os.linesep - purpose = "" - if self.command is not None: - purpose = self.command.get_purpose() + os.linesep - usage = f"Usage: `{self.get_usage()}`" - addendum = self.get_usage_addendum() - return name + purpose + usage + addendum - - def _send_message_response(self, response_message, slack_event, as_reply=False): - reply_ts = "" - if as_reply: # Force reply in thread - reply_ts = slack_event["ts"] - if "thread_ts" in slack_event: # We're already in a thread so stay there - reply_ts = slack_event["thread_ts"] - self._slack_message_broker.send_message(response_message, slack_event["channel"], reply_ts) - - def _send_reaction_response(self, slack_event): - self._slack_message_broker.send_reaction(slack_event) - - def _send_dm_response(self, response_message, slack_event): - self._slack_message_broker.send_dm(response_message, slack_event["user_id"]) - - def _get_config(self): - config = Config() - self.token = config.get_config_value(ConfigKeys.slack_bot_token) - self.command_trigger = config.get_prefix() + self._get_command_symbol() + " " - - def __init__(self, debug=False): - self._slack_message_broker = SlackMessageBroker(debug) - self.debug = debug - self._get_config() diff --git a/slack_channel/abstract_reaction_handler.py b/slack_channel/abstract_reaction_handler.py deleted file mode 100644 index 56aed5a..0000000 --- a/slack_channel/abstract_reaction_handler.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -import logging.config -import os -from os import path -from abc import ABC, abstractmethod - -from commands.abstract_command import AbstractCommand -from config import Config, ConfigKeys -from slack_channel.slack_message_broker import SlackMessageBroker - -log_file_path = path.join(path.dirname(path.abspath(__file__)), '../logging.conf') -logging.config.fileConfig(log_file_path) -logger = logging.getLogger('bottomly') - -class AbstractReactionHandler(ABC): - - @property - @abstractmethod - def command(self) -> AbstractCommand: - pass - - @abstractmethod - def can_handle(self, reaction_add_event) -> bool: - pass - - @abstractmethod - def _invoke_handler_logic(self, reaction_add_event): - pass - - def handle(self, reaction_add_event): - logger.info(str(self) + " Handling: " + str(reaction_add_event)) - try: - if self.can_handle(reaction_add_event): - self._invoke_handler_logic(reaction_add_event) - except Exception: - logger.exception("Error thrown handling event: " + reaction_add_event) - - def _send_reaction_response(self, reaction_add_event): - self._slack_message_broker.send_reaction(reaction_add_event["item"]) - - def _get_config(self): - config = Config() - self.token = config.get_config_value(ConfigKeys.slack_bot_token) - - def __init__(self, debug=False): - self._slack_message_broker = SlackMessageBroker(debug) - self.debug = debug - self._get_config() diff --git a/slack_channel/add_karma_event_handler.py b/slack_channel/add_karma_event_handler.py deleted file mode 100644 index 6785a6a..0000000 --- a/slack_channel/add_karma_event_handler.py +++ /dev/null @@ -1,141 +0,0 @@ -import logging -from abc import abstractmethod - -from commands import AddKarmaCommand -from config import Config -from model.karma import KarmaType -from model.member import Member -from slack_channel.abstract_event_handler import AbstractEventHandler -from slack_channel.slack_parser import SlackParser - - -class AbstractKarmaEventHandler(AbstractEventHandler): - - FOR_STRING = " for " - - @property - @abstractmethod - def name(self) -> str: - pass - - @abstractmethod - def _get_command_symbol(self) -> str: - pass - - @property - def command(self) -> AddKarmaCommand: - return AddKarmaCommand() - - @property - @abstractmethod - def karma_type(self) -> KarmaType: - pass - - def get_usage(self): - return self._get_command_symbol() + " recipient [[for ] reason]" - - @property - def _help_message(self): - return self._get_command_symbol() + " -?" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self._get_command_symbol()) - - def _invoke_handler_logic(self, slack_event): - try: - command_text = slack_event['text'] - args = self._parse_command_text(command_text) - self.command.execute(awarded_to=args["recipient"], - awarded_by=slack_event["user"], - reason=args["reason"], - karma_type=args["karma_type"]) - self._send_reaction_response(slack_event) - except Exception as ex: - logging.exception(ex) - - def _parse_command_text(self, command_text): - command_text = SlackParser.replace_slack_id_tokens_with_usernames(command_text) - command_text = command_text[len(self._get_command_symbol()):].lstrip() - - command_split = command_text.split(self.FOR_STRING) # Attempt to tokenise on "for" - if len(command_split) > 1: # If there's a "for" then life is easy - recipient = command_split[0] # First token is the username - else: - recipient = self._parse_recipient(command_text) # Get the recipient out - - # Get the reason by removing the first occurrence of the recipient from the command_text - reason = command_text.replace(recipient, "", 1) - if reason.startswith(self.FOR_STRING): - # If Command starts with for then remove it - reason = reason.replace(self.FOR_STRING, "", 1).lstrip() - else: - reason = reason.lstrip() - - return {"recipient": recipient, "reason": reason, "karma_type": self.karma_type} - - def _parse_recipient(self, command_text): - possible_username = command_text.split(" ")[0] # split on spaces, first word is a possible user - decided_username = command_text # assume we're going to be using the full command_text as the username - - username_is_known = self._username_is_known(possible_username) - if username_is_known: # if user is known in the DB then it is a usable name - decided_username = possible_username - - return decided_username - - @staticmethod - def _username_is_known(username): - m = Member.get_member_by_username(username) - if m is not None: - return True - else: - return False - - @staticmethod - def _parse_reason(command_text, recipient): - recipient_length = len(recipient) + 1 # +1 to account for space - if command_text.find(" for ") != -1: - recipient_length += 4 - reason = command_text[recipient_length:] - return reason - - def __init__(self, debug=False): - self.config = Config() - self.config.connect_to_db() - super(AbstractKarmaEventHandler, self).__init__(debug) - - -class IncrementKarmaEventHandler(AbstractKarmaEventHandler): - - @property - def name(self): - return "Pozzy-poz" - - def _get_command_symbol(self): - return "++" - - @property - def karma_type(self): - return KarmaType.POZZYPOZ - - def __init__(self, debug=False): - super(IncrementKarmaEventHandler, self).__init__(debug) - - -class DecrementKarmaEventHandler(AbstractKarmaEventHandler): - - @property - def name(self): - return "Neggy-neg" - - def _get_command_symbol(self): - return "--" - - @property - def karma_type(self): - return KarmaType.NEGGYNEG - - def __init__(self, debug=False): - self.debug = debug - super(DecrementKarmaEventHandler, self).__init__(debug) diff --git a/slack_channel/add_karma_reaction_handler.py b/slack_channel/add_karma_reaction_handler.py deleted file mode 100644 index 216883c..0000000 --- a/slack_channel/add_karma_reaction_handler.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging - -from commands import AddKarmaCommand -from slack_channel.abstract_reaction_handler import AbstractReactionHandler - -class AddKarmaReactionHandler(AbstractReactionHandler): - - @property - def command(self) -> AddKarmaCommand: - return AddKarmaCommand() - - def can_handle(self, reaction_add_event) -> bool: - reaction = self.parse_reaction(reaction_add_event["reaction"]) - return reaction in AddKarmaCommand.get_karma_reactions() - - def _invoke_handler_logic(self, reaction_add_event): - reactions = AddKarmaCommand.get_karma_reactions() - try: - reaction = self.parse_reaction(reaction_add_event["reaction"]) - self.command.execute(awarded_to=reaction_add_event["reactee"], - awarded_by=reaction_add_event["reactor"], - reason="Reacted with " + reaction, - karma_type=reactions[reaction]) - self._send_reaction_response(reaction_add_event) - except Exception as ex: - logging.exception(ex) - - def parse_reaction(self, raw_reaction): - return raw_reaction.split('::')[0] diff --git a/slack_channel/cocktail_of_the_week_event_handler.py b/slack_channel/cocktail_of_the_week_event_handler.py deleted file mode 100644 index 51493b5..0000000 --- a/slack_channel/cocktail_of_the_week_event_handler.py +++ /dev/null @@ -1,34 +0,0 @@ -# coding=utf-8 -from commands import CocktailOfTheWeekSearchCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "cotw" -empty_result_message = "You're on your own I'm afraid - https://thebrassmonkeygla.co.uk/." - - -class CocktailOfTheWeekEventHandler(AbstractEventHandler): - @property - def command(self) -> CocktailOfTheWeekSearchCommand: - return CocktailOfTheWeekSearchCommand() - - @property - def name(self): - return "Cocktail of the week lookup from the Brass Monkey" - - def get_usage(self): - return self.command_trigger - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) # No trailing space because no arguments for this. - - def _invoke_handler_logic(self, slack_event): - response_message = self.command.execute() - if response_message is None: - self._send_message_response(empty_result_message, slack_event) - else: - self._send_message_response(response_message, slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/get_current_karma_reasons_event_handler.py b/slack_channel/get_current_karma_reasons_event_handler.py deleted file mode 100644 index 477b46d..0000000 --- a/slack_channel/get_current_karma_reasons_event_handler.py +++ /dev/null @@ -1,60 +0,0 @@ -import os - -from commands import GetCurrentKarmaReasonsCommand -from model.karma import KarmaType -from slack_channel.abstract_event_handler import AbstractEventHandler -from slack_channel.slack_parser import SlackParser - -command_symbol = "reasons" - - -def _build_response(result, recipient) -> str: - karma_keys = {str(KarmaType.POZZYPOZ): "++", str(KarmaType.NEGGYNEG): "--"} - response = "Recent Karma for " + recipient + ":" - response += os.linesep - response += "Recently awarded with no reason: " + str(result["reasonless"]) + "." - karma_with_reasons = result["reasoned"] - if len(karma_with_reasons) == 0: - response += os.linesep - response += "None awarded with a reason given." - return response - - for k in karma_with_reasons: - response += os.linesep - response += karma_keys[k.karma_type] + " " - response += "from " + k.awarded_by_username + " " - response += 'for "' + k.reason + '"' - - return response - - -class GetCurrentKarmaReasonsEventHandler(AbstractEventHandler): - @property - def command(self) -> GetCurrentKarmaReasonsCommand: - return GetCurrentKarmaReasonsCommand() - - @property - def name(self): - return "Karma Reasons" - - def _get_command_symbol(self): - return command_symbol - - def get_usage(self): - return self.command_trigger + "[recipient ]" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) # Trim the space off in case of no recipient - - def _invoke_handler_logic(self, slack_event): - message = SlackParser.replace_slack_id_tokens_with_usernames(slack_event["text"]) - recipient = message[len(self.command_trigger):].split(' ')[0] - - if recipient == "": - recipient = slack_event["user"] - - result = self.command.execute(recipient) - - response = _build_response(result, recipient) - self._send_dm_response(response, slack_event) diff --git a/slack_channel/get_current_net_karma_event_handler.py b/slack_channel/get_current_net_karma_event_handler.py deleted file mode 100644 index 62cb7b2..0000000 --- a/slack_channel/get_current_net_karma_event_handler.py +++ /dev/null @@ -1,52 +0,0 @@ -import os - -from commands import GetCurrentNetKarmaCommand, AddKarmaCommand -from model.karma import KarmaType -from slack_channel.abstract_event_handler import AbstractEventHandler -from slack_channel.slack_parser import SlackParser - - -command_symbol = "karma" - - -class GetCurrentNetKarmaEventHandler(AbstractEventHandler): - @property - def command(self) -> GetCurrentNetKarmaCommand: - return GetCurrentNetKarmaCommand() - - @property - def name(self): - return "Get Current Karma" - - def _get_command_symbol(self): - return command_symbol - - def get_usage(self): - return self.command_trigger + "[recipient ]" - - def get_usage_addendum(self): - karma_reactions = AddKarmaCommand.get_karma_reactions() - karma_descriptions = { - KarmaType.POZZYPOZ: "PozzyPoz", - KarmaType.NEGGYNEG: "NeggyNeg" - } - response = f"{os.linesep}Giving Karma with reactions: {os.linesep}" - for reaction_key in karma_reactions.keys(): - response += f":{reaction_key}: will {karma_descriptions[karma_reactions[reaction_key]]}{os.linesep}" - return response - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) # Trim the space off in case of no recipient - - def _invoke_handler_logic(self, slack_event): - message = SlackParser.replace_slack_id_tokens_with_usernames(slack_event["text"]) - recipient = message[len(self.command_trigger):].split(' ')[0] - - if recipient == "": - recipient = slack_event["user"] - - result = self.command.execute(recipient) - - response_message = recipient + ": " + str(result) - self._send_message_response(response_message, slack_event) diff --git a/slack_channel/get_leader_board_event_handler.py b/slack_channel/get_leader_board_event_handler.py deleted file mode 100644 index 5e473f8..0000000 --- a/slack_channel/get_leader_board_event_handler.py +++ /dev/null @@ -1,48 +0,0 @@ -import os - -from commands import GetLeaderBoardCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "leaderboard" - - -def _build_response(result: list) -> str: - # result example: [{"username": "", "net_karma": }] - response = "" - for r in result: - response += "{username}: {net_karma}".format(username=r["username"], net_karma=r["net_karma"]) - response += os.linesep - return response.strip() - - -class GetLeaderBoardEventHandler(AbstractEventHandler): - @property - def command(self) -> GetLeaderBoardCommand: - return GetLeaderBoardCommand() - - @property - def name(self): - return "Get Leaderboard" - - def _get_command_symbol(self): - return command_symbol - - def get_usage(self): - return self.command_trigger + "[size of leaderboard. Default is 3]" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) # Trim the space off in case of no size arg - - def _invoke_handler_logic(self, slack_event): - message = slack_event["text"] - size = 3 - size_arg = message[len(self.command_trigger):] - if size_arg.isdigit() and int(size_arg) > 0: - size = int(size_arg) - - result = self.command.execute(size) - - response_message = _build_response(result) - self._send_message_response(response_message, slack_event) diff --git a/slack_channel/get_loser_board_event_handler.py b/slack_channel/get_loser_board_event_handler.py deleted file mode 100644 index 21a46da..0000000 --- a/slack_channel/get_loser_board_event_handler.py +++ /dev/null @@ -1,48 +0,0 @@ -import os - -from commands import GetLoserBoardCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "loserboard" - - -def _build_response(result: list) -> str: - # result example: [{"username": "", "net_karma": }] - response = "" - for r in result: - response += "{username}: {net_karma}".format(username=r["username"], net_karma=r["net_karma"]) - response += os.linesep - return response.strip() - - -class GetLoserBoardEventHandler(AbstractEventHandler): - @property - def command(self) -> GetLoserBoardCommand: - return GetLoserBoardCommand() - - @property - def name(self): - return "Get Loserboard" - - def _get_command_symbol(self): - return command_symbol - - def get_usage(self): - return self.command_trigger + "[size of loserboard. Default is 3]" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) # Trim the space off in case of no size arg - - def _invoke_handler_logic(self, slack_event): - message = slack_event["text"] - size = 3 - size_arg = message[len(self.command_trigger):] - if size_arg.isdigit() and int(size_arg) > 0: - size = int(size_arg) - - result = self.command.execute(size) - - response_message = _build_response(result) - self._send_message_response(response_message, slack_event) diff --git a/slack_channel/giphy_event_handler.py b/slack_channel/giphy_event_handler.py deleted file mode 100644 index 05c8ab9..0000000 --- a/slack_channel/giphy_event_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding=utf-8 -from commands import GiphyTranslateCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "gif" - - -class GiphyEventHandler(AbstractEventHandler): - @property - def command(self) -> GiphyTranslateCommand: - return GiphyTranslateCommand() - - @property - def name(self): - return "Giphy" - - def get_usage(self): - return self.command_trigger + "" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger) - - def _invoke_handler_logic(self, slack_event): - q = slack_event["text"][len(self.command_trigger):] - result = self.command.execute(q) - if result is None: - response_message = "No gifs found for \"" + q + "\"" - else: - response_message = result - self._send_message_response(response_message, slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/google_event_handler.py b/slack_channel/google_event_handler.py deleted file mode 100644 index 8e1a1a7..0000000 --- a/slack_channel/google_event_handler.py +++ /dev/null @@ -1,34 +0,0 @@ -# coding=utf-8 -from commands import GoogleSearchCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - -command_symbol = "g" - - -class GoogleEventHandler(AbstractEventHandler): - @property - def command(self) -> GoogleSearchCommand: - return GoogleSearchCommand() - - @property - def name(self): - return "Google" - - def get_usage(self): - return self.command_trigger + "" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger) - - def _invoke_handler_logic(self, slack_event): - q = slack_event["text"][len(self.command_trigger):] - result = self.command.execute(q) - if result is None: - response_message = "No results found for \"" + q + "\"" - else: - response_message = result["title"] + " " + result["link"] - self._send_message_response(response_message, slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/google_image_event_handler.py b/slack_channel/google_image_event_handler.py deleted file mode 100644 index ccf25ad..0000000 --- a/slack_channel/google_image_event_handler.py +++ /dev/null @@ -1,34 +0,0 @@ -# coding=utf-8 -from commands import GoogleImageSearchCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - -command_symbol = "gi" - - -class GoogleImageEventHandler(AbstractEventHandler): - @property - def command(self) -> GoogleImageSearchCommand: - return GoogleImageSearchCommand() - - @property - def name(self): - return "Google Image" - - def get_usage(self): - return self.command_trigger + "" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger) - - def _invoke_handler_logic(self, slack_event): - q = slack_event["text"][len(self.command_trigger):] - result = self.command.execute(q) - if result is None: - response_message = "No results found for \"" + q + "\"" - else: - response_message = result - self._send_message_response(response_message, slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/help_event_handler.py b/slack_channel/help_event_handler.py deleted file mode 100644 index a06d439..0000000 --- a/slack_channel/help_event_handler.py +++ /dev/null @@ -1,54 +0,0 @@ -# coding=utf-8 -import os - -from commands.abstract_command import AbstractCommand -from config import Config -from slack_channel.abstract_event_handler import AbstractEventHandler - -command_symbols = ["help", "?", "list"] - - -class HelpEventHandler(AbstractEventHandler): - @property - def command(self) -> AbstractCommand: - pass - - @property - def name(self) -> str: - return "Help" - - @property - def _prefixed_command_symbols(self): - prefix = self.config.get_prefix() - return [prefix + c for c in command_symbols] - - def can_handle(self, slack_event) -> bool: - split = slack_event["text"].split(" ") - return split[0] in self._prefixed_command_symbols - - def _invoke_handler_logic(self, slack_event): - help_text = "" - for handler in self._command_handlers: - help_text += handler.build_help_message() + os.linesep - help_text = help_text.strip() - self._send_dm_response(help_text, slack_event) - - def _get_command_symbol(self) -> str: - return command_symbols[0] - - def get_usage(self) -> str: - usage = "" - for pc in self._prefixed_command_symbols: - usage += "`" + pc + "` or " - usage = usage[1:-5] - return usage - - def _get_config(self): - self.config = Config() - self.command_trigger = self.config.get_prefix() + self._get_command_symbol() + " " - - def __init__(self, debug=False, command_handlers=list([])): - super(HelpEventHandler, self).__init__(self) - self._command_handlers = command_handlers - self.debug = debug - self._get_config() diff --git a/slack_channel/initial_memberlist_populator.py b/slack_channel/initial_memberlist_populator.py deleted file mode 100644 index 4fa3760..0000000 --- a/slack_channel/initial_memberlist_populator.py +++ /dev/null @@ -1,46 +0,0 @@ -# coding=utf-8 -import logging -import logging.config -import os -from slacker import Slacker - -from commands.add_member import AddMemberCommand -from config import Config, ConfigKeys - - -token = Config().get_config_value(ConfigKeys.slack_bot_token) - -logging.config.fileConfig('./logging.conf') -logger = logging.getLogger('bottomly') - - -class InitialMemberlistPopulator(object): - - @staticmethod - def populate(): - try: - print("Beginning population...") - Config().connect_to_db() - slack = Slacker(token) - response = slack.users.list() - users = response.body['members'] - data = "" - for user in users: - if not user['deleted']: - username = user['name'] - slack_id = user['id'] - c = AddMemberCommand(username, slack_id) - c.execute() - data += "Added " + username + os.linesep - return data - except Exception as ex: - message = "Error running initial populate" - logger.exception(message) - print(message + ": " + ex) - - def __init__(self): - super(InitialMemberlistPopulator, self) - - -if __name__ == '__main__': - InitialMemberlistPopulator().populate() diff --git a/slack_channel/reaction_test_handler.py b/slack_channel/reaction_test_handler.py deleted file mode 100644 index e9311de..0000000 --- a/slack_channel/reaction_test_handler.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding=utf-8 -from commands import VoidCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "react" - - -class ReactionTestHandler(AbstractEventHandler): - @property - def command(self) -> VoidCommand: - return VoidCommand() - - @property - def name(self): - return "Reaction Test" - - def get_usage(self): - return self.command_trigger - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) - - def _invoke_handler_logic(self, slack_event): - self._send_reaction_response(slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/reg_event_handler.py b/slack_channel/reg_event_handler.py deleted file mode 100644 index 8ba8c3b..0000000 --- a/slack_channel/reg_event_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding=utf-8 -from commands import RegSearchCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "vroom" -empty_result_message = "Left as an exercise for the reader." - - -class RegEventHandler(AbstractEventHandler): - @property - def command(self) -> RegSearchCommand: - return RegSearchCommand() - - @property - def name(self): - return "AutoTrader Reg Lookup" - - def get_usage(self): - return self.command_trigger + "" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger) - - def _invoke_handler_logic(self, slack_event): - q = slack_event["text"][len(self.command_trigger):] - response_message = self.command.execute(q) - if response_message is None: - self._send_message_response(empty_result_message, slack_event) - else: - self._send_message_response(response_message, slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/release_event_handler.py b/slack_channel/release_event_handler.py deleted file mode 100644 index 7dc3516..0000000 --- a/slack_channel/release_event_handler.py +++ /dev/null @@ -1,32 +0,0 @@ -from commands import ReleaseCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "release" - - -class ReleaseEventHandler(AbstractEventHandler): - @property - def command(self) -> ReleaseCommand: - return ReleaseCommand() - - @property - def name(self): - return "Release" - - def get_usage(self): - return self.command_trigger[:-1] - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) - - def _invoke_handler_logic(self, slack_event): - release_desc = self.command.execute() - if release_desc is None: - self._send_message_response('Unable to retrieve latest release info.', slack_event) - else: - self._send_message_response(release_desc, slack_event) - - def _get_command_symbol(self): - return command_symbol \ No newline at end of file diff --git a/slack_channel/slack_event_handler.py b/slack_channel/slack_event_handler.py deleted file mode 100644 index c967d41..0000000 --- a/slack_channel/slack_event_handler.py +++ /dev/null @@ -1,246 +0,0 @@ -# coding=utf-8 -import datetime -import logging -import logging.config - -from threading import Event - -from slack_sdk import WebClient -from slack_sdk.rtm import RTMClient -from slack_sdk.socket_mode import SocketModeClient -from slack_sdk.errors import SlackApiError - -from slack_sdk.socket_mode.response import SocketModeResponse -from slack_sdk.socket_mode.request import SocketModeRequest - -from config import Config, ConfigKeys -from model.member import Member -from slack_channel import * - -live_mode = "live" - -config = Config() -token = config.get_config_value(ConfigKeys.slack_bot_token) -app_token = config.get_config_value(ConfigKeys.slack_app_token) -debug = Config().get_config_value(ConfigKeys.env_key) != live_mode - -logging.config.fileConfig('logging.conf') -logger = logging.getLogger('bottomly') -for handler in logger.handlers: - if debug: - handler.setLevel(logging.INFO) - else: - handler.setLevel(logging.WARN) - -_command_handlers = list([ - GoogleEventHandler(debug), - UrbanEventHandler(debug), - WikipediaEventHandler(debug), - IncrementKarmaEventHandler(debug), - DecrementKarmaEventHandler(debug), - GetCurrentNetKarmaEventHandler(debug), - GetCurrentKarmaReasonsEventHandler(debug), - GiphyEventHandler(debug), - GetLoserBoardEventHandler(debug), - GetLeaderBoardEventHandler(debug), - RegEventHandler(debug), - TestEventHandler(debug), - ReactionTestHandler(debug), - GoogleImageEventHandler(debug), - # CocktailOfTheWeekEventHandler(debug), - ReleaseEventHandler(debug) -]) - -_reaction_handlers = list([ - AddKarmaReactionHandler(debug) -]) - -_channel_list = WebClient(token=token).conversations_list().data.get("channels") - - -def _is_subscribed_message(slack_event): - try: - subscribed = True - subscribed = subscribed and slack_event["type"] == "message" - subscribed = subscribed and "text" in slack_event - subscribed = subscribed and "bot_id" not in slack_event - return subscribed - except Exception as ex: - logger.warning("Error determining if message is subscribed: " + str(ex)) - logger.warning("Message: " + str(slack_event)) - return False - - -def _is_subscribed_reaction(slack_event): - try: - subscribed = True - subscribed = subscribed and slack_event["type"] == "reaction_added" - return subscribed - except Exception as ex: - logger.warning("Error determining if reaction is subscribed: " + str(ex)) - logger.warning("Reaction Event: " + str(slack_event)) - return False - - -def run_slack_rtm_client(): - try: - logger.info("Opening RTM connection to slack.") - rtm_client = RTMClient(token=token) - rtm_client.start() - except SlackApiError: - logger.exception("Error establishing RTM connection to slack.") - -def run_slack_socket_mode_client(): - try: - logger.info("Opening Socket Mode connection to slack.") - socket_mode_client = SocketModeClient(app_token=app_token, web_client=WebClient(token=token)) - socket_mode_client.socket_mode_request_listeners.append(_process_slack_socket_message) - socket_mode_client.socket_mode_request_listeners.append(_process_slack_socket_reaction) - socket_mode_client.connect() - logger.info("Successfully connected to slack via Socket Mode.") - Event().wait() - except SlackApiError: - logger.exception("Error establishing Socket Mode connection to slack.") - -@RTMClient.run_on(event="message") -def _process_slack_message(**e): - try: - slack_event = e["data"] - if not _is_subscribed_message(slack_event): - return - - logger.info("Subscribing to: " + str(slack_event)) - - _insert_channel_id_to_message(slack_event) - _insert_username(slack_event) - - handled = False - - help_handler = HelpEventHandler(debug, _command_handlers) - if help_handler.can_handle(slack_event): - handled = help_handler.handle(slack_event) - - if handled: - return - - handled = _execute_command_handlers(slack_event) - - if handled: - return - - # TODO: Dynamic response handler - - except SlackApiError: - logger.exception("Error processing slack message") - -def _process_slack_socket_message(client: SocketModeClient, req: SocketModeRequest): - try: - slack_event = req.payload["event"] - if not _is_subscribed_message(slack_event): - _acknowledge_message(client, req) - return - - logger.info("Subscribing to: " + str(slack_event)) - - _insert_channel_id_to_message(slack_event) - _insert_username(slack_event) - - handled = False - - help_handler = HelpEventHandler(debug, _command_handlers) - if help_handler.can_handle(slack_event): - handled = help_handler.handle(slack_event) - - if handled: - _acknowledge_message(client, req) - return - - handled = _execute_command_handlers(slack_event) - - if handled: - _acknowledge_message(client, req) - return - - # TODO: Dynamic response handler - - except SlackApiError: - logger.exception("Error processing slack message") - _acknowledge_message(client, req) - - -def _acknowledge_message(client: SocketModeClient, req: SocketModeRequest): - response = SocketModeResponse(envelope_id=req.envelope_id) - client.send_socket_mode_response(response) - -@RTMClient.run_on(event="reaction_added") -def _process_slack_reaction(**e): - try: - slack_event = e["data"] - if not _is_subscribed_reaction(slack_event): - return - slack_event["channel"] = slack_event["item"]["channel"] - - slack_event["reactor"] = Member.get_member_by_slack_id(slack_event["user"]).username - slack_event["reactee"] = Member.get_member_by_slack_id(slack_event["item_user"]).username - - logger.info(str(slack_event)) - - _reaction_handlers[0].handle(slack_event) - - except SlackApiError: - logger.exception("Slack API Error processing slack reaction") - - except Exception as ex: - logger.exception("General error processing slack reaction: " + str(ex)) - -def _process_slack_socket_reaction(client: SocketModeClient, req: SocketModeRequest): - try: - slack_event = req.payload["event"] - if not _is_subscribed_reaction(slack_event): - _acknowledge_message(client, req) - return - slack_event["channel"] = slack_event["item"]["channel"] - - slack_event["reactor"] = Member.get_member_by_slack_id(slack_event["user"]).username - slack_event["reactee"] = Member.get_member_by_slack_id(slack_event["item_user"]).username - - logger.info(str(slack_event)) - - _reaction_handlers[0].handle(slack_event) - _acknowledge_message(client, req) - - except SlackApiError: - logger.exception("Slack API Error processing slack reaction") - - except Exception as ex: - logger.exception("General error processing slack reaction: " + str(ex)) - - -def _execute_command_handlers(slack_event): - handled = False - for handler in _command_handlers: - if handler.can_handle(slack_event): - handler.handle(slack_event) - handled = True - continue - return handled - - -def _insert_channel_id_to_message(message_event): - try: - known_channel_names = [c["name"] for c in _channel_list] - if message_event["channel"] in known_channel_names: - channel = [c for c in _channel_list if c["name"] == message_event["channel"]][0] - message_event["channel_id"] = channel["id"] - except Exception: - logger.exception("Error loading channel id from message: " + str(message_event)) - - -def _insert_username(slack_event): - try: - member = Member.get_member_by_slack_id(slack_event["user"]) - slack_event["user_id"] = slack_event["user"] - slack_event["user"] = member.username - - except Exception: - logger.exception("Error loading user id from event: " + str(slack_event)) diff --git a/slack_channel/slack_message_broker.py b/slack_channel/slack_message_broker.py deleted file mode 100644 index c725d98..0000000 --- a/slack_channel/slack_message_broker.py +++ /dev/null @@ -1,48 +0,0 @@ -# coding=utf-8 -import logging -from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError - -from config import Config, ConfigKeys - - -class SlackMessageBroker(object): - - def send_reaction(self, slack_event_to_react_to): - logging.info("Sending reaction") - try: - client = WebClient(token=self.token) - client.reactions_add(name="robot_face", - channel=slack_event_to_react_to["channel"], - timestamp=slack_event_to_react_to["ts"]) - except SlackApiError: - logging.exception("Error sending reaction to: " + str(slack_event_to_react_to)) - - def send_message(self, message_text, channel, in_reply_to_ts=None): - if message_text is None or message_text == "": - return - if self.debug: - message_text = f"[{self.environment}] {message_text}" - try: - client = WebClient(token=self.token) - msg_response = client.chat_postMessage(channel=channel, thread_ts=in_reply_to_ts, text=message_text) - logging.info("Sent message: " + str(msg_response) + ", to channel: " + channel) - except SlackApiError: - logging.exception("Error sending message response to: " + channel) - - def send_dm(self, message_text, user_slack_id): - if message_text is None or message_text == "": - return - try: - client = WebClient(token=self.token) - im = client.conversations_open(users=user_slack_id) - channel_id = im["channel"]["id"] - self.send_message(message_text, channel_id) - except SlackApiError: - logging.exception("Error sending DM response to: " + str(user_slack_id)) - - def __init__(self, debug=False): - self.debug = debug - config = Config() - self.token = config.get_config_value(ConfigKeys.slack_bot_token) - self.environment = config.get_config_value(ConfigKeys.env_key) diff --git a/slack_channel/slack_parser.py b/slack_channel/slack_parser.py deleted file mode 100644 index 371e45d..0000000 --- a/slack_channel/slack_parser.py +++ /dev/null @@ -1,34 +0,0 @@ -from model.member import Member - -def _is_slack_id(word): - return word.startswith('<@') and word.endswith('>') - -def _strip_slack_token_formatting(word): - return word[2:len(word) - 1] - -def _convert_slack_id_to_username(slack_id): - m = Member.get_member_by_slack_id(slack_id) - if m is not None: - return m.username - raise Exception("User not found") - - -class SlackParser(object): - - @staticmethod - def replace_slack_id_tokens_with_usernames(slack_message): - words = slack_message.split(' ') - new_words = list([]) - for word in words: - new_word = word - if _is_slack_id(word): - new_word = _strip_slack_token_formatting(word) - new_word = _convert_slack_id_to_username(new_word) - new_words.append(new_word) - - if slack_message.find(' ') != -1: - updated_message = ' '.join(new_words) - else: - updated_message = new_words[0] - - return updated_message \ No newline at end of file diff --git a/slack_channel/test_event_handler.py b/slack_channel/test_event_handler.py deleted file mode 100644 index 79e0f6a..0000000 --- a/slack_channel/test_event_handler.py +++ /dev/null @@ -1,30 +0,0 @@ -# coding=utf-8 -from commands import VoidCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "test" - - -class TestEventHandler(AbstractEventHandler): - @property - def command(self) -> VoidCommand: - return VoidCommand() - - @property - def name(self): - return "Test" - - def get_usage(self): - return self.command_trigger - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger[:-1]) - - def _invoke_handler_logic(self, slack_event): - self.command.execute() - self._send_message_response("OK", slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/urban_event_handler.py b/slack_channel/urban_event_handler.py deleted file mode 100644 index 1a0dd25..0000000 --- a/slack_channel/urban_event_handler.py +++ /dev/null @@ -1,41 +0,0 @@ -# coding=utf-8 -from commands import UrbanSearchCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "ud" -empty_result_message = "Left as an exercise for the reader." - - -class UrbanEventHandler(AbstractEventHandler): - @property - def command(self) -> UrbanSearchCommand: - return UrbanSearchCommand() - - @property - def name(self): - return "Urban Dictionary" - - def get_usage(self): - return self.command_trigger + "" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger) - - def _invoke_handler_logic(self, slack_event): - q = slack_event["text"][len(self.command_trigger):] - response_message = self.command.execute(q) - if response_message is None: - self._send_message_response( - response_message=empty_result_message, - slack_event=slack_event, - as_reply=True) - else: - self._send_message_response( - response_message=response_message, - slack_event=slack_event, - as_reply=True) - - def _get_command_symbol(self): - return command_symbol diff --git a/slack_channel/wikipedia_event_handler.py b/slack_channel/wikipedia_event_handler.py deleted file mode 100644 index 98bd1a6..0000000 --- a/slack_channel/wikipedia_event_handler.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding=utf-8 -from commands import WikipediaSearchCommand -from slack_channel.abstract_event_handler import AbstractEventHandler - - -command_symbol = "wik" - - -class WikipediaEventHandler(AbstractEventHandler): - @property - def command(self) -> WikipediaSearchCommand: - return WikipediaSearchCommand() - - @property - def name(self): - return "Wikipedia" - - def get_usage(self): - return self.command_trigger + "" - - def can_handle(self, slack_event): - text = slack_event["text"] - return text.startswith(self.command_trigger) - - def _invoke_handler_logic(self, slack_event): - q = slack_event["text"][len(self.command_trigger):] - result = self.command.execute(q) - if result is None: - response_message = "No results found for \"" + q + "\"" - else: - response_message = result["text"] + " " + result["link"] - self._send_message_response(response_message, slack_event) - - def _get_command_symbol(self): - return command_symbol diff --git a/tag.ps1 b/tag.ps1 deleted file mode 100644 index 783e71a..0000000 --- a/tag.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -az acr login --name BottomlyContainerReg -$tag = git log --pretty=format:%h -n 1 -docker tag bottomly bottomlycontainerreg.azurecr.io/docker-bottomly:$tag -docker push bottomlycontainerreg.azurecr.io/docker-bottomly:$tag -docker images \ No newline at end of file diff --git a/test.dockerfile b/test.dockerfile deleted file mode 100644 index f81da2b..0000000 --- a/test.dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM python:3 -COPY . /app -WORKDIR /app -RUN pip install -r requirements.txt -CMD python -m unittest discover \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/command_tests/__init__.py b/tests/command_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/command_tests/test_add_karma.py b/tests/command_tests/test_add_karma.py deleted file mode 100644 index fe2e468..0000000 --- a/tests/command_tests/test_add_karma.py +++ /dev/null @@ -1,41 +0,0 @@ -import unittest - -from datetime import datetime - -from commands.add_karma import AddKarmaCommand -from config import Config -from model.karma import Karma, KarmaType - - -class TestAddKarma(unittest.TestCase): - - def test_persistence(self): - # Arrange - Config().connect_to_db() - awarded_to = "testUser1" - awarded_by = "testUser2" - test_reason = "testReason" - test_type = KarmaType.POZZYPOZ - k = Karma(awarded_to_username=awarded_to, - reason=test_reason, - awarded_by_username=awarded_by, - awarded=datetime.now(), - karma_type=str(test_type)) - - # Act - c = AddKarmaCommand() - saved_karma = c.execute(k.awarded_to_username, k.awarded_by_username, k.reason, k.karma_type) - - # Assert - self.assertIsNotNone(saved_karma._id) - self.assertEqual(k.awarded_by_username, saved_karma.awarded_by_username) - self.assertEqual(k.reason, saved_karma.reason) - self.assertEqual(k.awarded_to_username, saved_karma.awarded_to_username) - self.assertEqual(k.karma_type, saved_karma.karma_type) - self.assertIsNotNone(saved_karma.awarded) - - # Tear down - saved_karma.delete() - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/command_tests/test_cocktail_of_the_week_search.py b/tests/command_tests/test_cocktail_of_the_week_search.py deleted file mode 100644 index 9e73591..0000000 --- a/tests/command_tests/test_cocktail_of_the_week_search.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -import unittest - -from commands.cocktail_of_the_week_search import CocktailOfTheWeekSearchCommand - - -@unittest.skip("This command is broken to due a change in the output format from The Brass Monkey's website.") -class TestCocktailOfTheWeekSearchCommand(unittest.TestCase): - - def test_empty_input(self): - command = CocktailOfTheWeekSearchCommand() - result = command.execute() - - results = result.splitlines(); - - self.assertEqual(len(results), 3) - self.assertEqual(results[0], 'Cocktail of the Week') - -if __name__ == '__main__': - unittest.main() diff --git a/tests/command_tests/test_get_current_karma_reasons.py b/tests/command_tests/test_get_current_karma_reasons.py deleted file mode 100644 index 8ba90e3..0000000 --- a/tests/command_tests/test_get_current_karma_reasons.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from unittest.mock import patch - -from commands import GetCurrentKarmaReasonsCommand -from model.karma import Karma - - -class TestGetCurrentNetKarmaCommand(unittest.TestCase): - - def test_get_net_karma_any_input(self): - expected_reasons = {'reasonless': 10, 'reasoned': [Karma(), Karma()]} - username = "test_username" - with patch.object(Karma, "get_current_karma_reasons_for_recipient", - return_value=expected_reasons) as test_method: - c = GetCurrentKarmaReasonsCommand() - actual_reasons = c.execute(username) - - self.assertEqual(expected_reasons, actual_reasons) - test_method.assert_called_once_with(username) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/command_tests/test_get_current_net_karma.py b/tests/command_tests/test_get_current_net_karma.py deleted file mode 100644 index 0c536f6..0000000 --- a/tests/command_tests/test_get_current_net_karma.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest - -from unittest.mock import patch - -from commands import GetCurrentNetKarmaCommand -from model.karma import Karma - - -class TestGetCurrentNetKarmaCommand(unittest.TestCase): - - def test_get_net_karma_any_input(self): - expected_karma = 10 - username = "test_username" - with patch.object(Karma, "get_current_net_karma_for_recipient", return_value=expected_karma) as test_method: - c = GetCurrentNetKarmaCommand() - actual_karma = c.execute(username) - - self.assertEqual(expected_karma, actual_karma) - test_method.assert_called_once_with(username) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/command_tests/test_get_leader_board.py b/tests/command_tests/test_get_leader_board.py deleted file mode 100644 index 39ea146..0000000 --- a/tests/command_tests/test_get_leader_board.py +++ /dev/null @@ -1,44 +0,0 @@ -# coding=utf-8 -import unittest - -from unittest.mock import patch - -from commands import GetLeaderBoardCommand -from model.karma import Karma - - -class TestGetLeaderBoard(unittest.TestCase): - def test_get_leader_board(self): - # Arrange - expected = [{"username": "cool guy", "net_karma": 2}, - {"username": "guy 1", "net_karma": 1}, - {"username": "guy 2", "net_karma": 1}] - - with patch.object(Karma, "get_leader_board", return_value=expected) as execution_method: - # Act - c = GetLeaderBoardCommand() - leader_board = c.execute() - - # Assert - execution_method.assert_called_once_with(size=3) - self.assertEqual(expected, leader_board) - - def test_get_leader_board_size_specified(self): - # Arrange - expected = [{"username": "cool guy", "net_karma": 2}, - {"username": "guy 1", "net_karma": 1}, - {"username": "guy 2", "net_karma": 1}, - {"username": "loser", "net_karma": -1}] - - # Act - with patch.object(Karma, "get_leader_board", return_value=expected) as execution_method: - c = GetLeaderBoardCommand() - leader_board = c.execute(size=4) - - # Assert - execution_method.assert_called_once_with(size=4) - self.assertEqual(expected, leader_board) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/command_tests/test_giphy_translate.py b/tests/command_tests/test_giphy_translate.py deleted file mode 100644 index 88891f5..0000000 --- a/tests/command_tests/test_giphy_translate.py +++ /dev/null @@ -1,20 +0,0 @@ -import unittest -from commands import GiphyTranslateCommand - - -class TestGiphyTranslateCommand(unittest.TestCase): - def test_empty_input(self): - command = GiphyTranslateCommand() - result = command.execute('') - - self.assertIsNone(result) - - def test_valid_input(self): - command = GiphyTranslateCommand() - result = command.execute("puppers and kittehs") - - self.assertIsNotNone(result) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/command_tests/test_google_image_search.py b/tests/command_tests/test_google_image_search.py deleted file mode 100644 index 35fbf21..0000000 --- a/tests/command_tests/test_google_image_search.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest -from commands.google_image_search import GoogleImageSearchCommand - - -class TestGoogleImageSearchCommand(unittest.TestCase): - def test_empty_input(self): - command = GoogleImageSearchCommand() - result = command.execute('') - - self.assertIsNone(result) - - def test_garbage_input(self): - command = GoogleImageSearchCommand() - result = command.execute('asdhbhklasdvfbuioasvfbhuilasdvfbhjkl') - - self.assertIsNone(result) - - def test_valid_input(self): - command = GoogleImageSearchCommand() - result = command.execute("doctor who") - self.assertIsNotNone(result) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/command_tests/test_google_search.py b/tests/command_tests/test_google_search.py deleted file mode 100644 index e6461bf..0000000 --- a/tests/command_tests/test_google_search.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest -from commands.google_search import GoogleSearchCommand - - -class TestGoogleSearchCommand(unittest.TestCase): - def test_empty_input(self): - command = GoogleSearchCommand() - result = command.execute('') - - self.assertIsNone(result) - - def test_garbage_input(self): - command = GoogleSearchCommand() - result = command.execute('asdhbhklasdvfbuioasvfbhuilasdvfbhjkl') - - self.assertIsNone(result) - - def test_valid_input(self): - command = GoogleSearchCommand() - result = command.execute("stackoverflow site:en.wikipedia.org") - - self.assertIsNotNone(result) - self.assertEqual('Stack overflow - Wikipedia'.lower(), result['title'].lower()) - self.assertEqual('https://en.wikipedia.org/wiki/Stack_overflow'.lower(), result['link'].lower()) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/command_tests/test_reg_search.py b/tests/command_tests/test_reg_search.py deleted file mode 100644 index fc0a990..0000000 --- a/tests/command_tests/test_reg_search.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest - -from commands.reg_search import RegSearchCommand - - -@unittest.skip("This command is broken to due a change in the output format from AutoTrader's lookup.") -class TestRegSearchCommand(unittest.TestCase): - - def test_empty_input(self): - command = RegSearchCommand() - result = command.execute('') - - self.assertEqual(result, 'Registration missing') - - def test_input_too_long(self): - command = RegSearchCommand() - result = command.execute('a random string long string') - - self.assertEqual(result, 'Registration too long.') - - def test_input_has_Special_Characters(self): - command = RegSearchCommand() - result = command.execute('Registration should not contain special characters') - - self.assertEqual(result, 'Registration too long.') - - def test_valid_input_upper_case(self): - command = RegSearchCommand() - result = command.execute('7 YO') - - self.assertEqual(result, 'Yellow Mclaren p1 Unknown (January 2015) https://www.vehiclecheck.co.uk/VehicleCheck/ShowImage/?id=&CapType=0') - - def test_valid_input_i_to_1(self): - command = RegSearchCommand() - result = command.execute('i reg') - - self.assertEqual(result, 'Black Mclaren 720S V8 S-A (September 2018) https://www.vehiclecheck.co.uk/VehicleCheck/ShowImage/?id=5BCE18EBB6DE1598&CapType=0') - - def test_failed_lookup(self): - command = RegSearchCommand() - result = command.execute('BMT 216A') - - self.assertEqual(result, 'Sorry, we didn\'t recognise that registration.') - - def test_successful_lookup_rk66ro(self): - command = RegSearchCommand() - result = command.execute('rk 66kro') - - self.assertEqual(result, 'Yellow Ferrari Laferrari Ad S-A (January 2017) https://www.vehiclecheck.co.uk/VehicleCheck/ShowImage/?id=&CapType=0') - - def test_successful_lookup_nothing_found(self): - command = RegSearchCommand() - result = command.execute('f1') - - self.assertEqual(result, 'Sorry, we do not have enough information on this vehicle.') - - def test_successful_lookup_missing_info(self): - command = RegSearchCommand() - result = command.execute('LF68 GXY') - - self.assertEqual(result, 'Sorry, we do not have enough information on this vehicle.') - -if __name__ == '__main__': - unittest.main() diff --git a/tests/command_tests/test_release_command.py b/tests/command_tests/test_release_command.py deleted file mode 100644 index 7addff4..0000000 --- a/tests/command_tests/test_release_command.py +++ /dev/null @@ -1,12 +0,0 @@ -import unittest - -from commands.release import ReleaseCommand - -class TestReleaseCommand(unittest.TestCase): - def test_empty_input(self): - command = ReleaseCommand() - result = command.execute() - self.assertIsNotNone(result) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/command_tests/test_urban_search.py b/tests/command_tests/test_urban_search.py deleted file mode 100644 index 79c297e..0000000 --- a/tests/command_tests/test_urban_search.py +++ /dev/null @@ -1,33 +0,0 @@ -import unittest -from commands.urban_search import UrbanSearchCommand - - -class TestUrbanSearchCommand(unittest.TestCase): - def test_empty_input(self): - command = UrbanSearchCommand() - result = command.execute('') - - self.assertIsNone(result) - - def test_garbage_input(self): - command = UrbanSearchCommand() - result = command.execute('asdhbhklasdvfbuioasvfbhuilasdvfbhjkl') - - self.assertIsNone(result) - - def test_valid_input(self): - command = UrbanSearchCommand() - result = command.execute("python") - - self.assertIsNotNone(result) - - def test_multi_word_search(self): - command = UrbanSearchCommand() - result = command.execute("hugh laurie") - - print(result) - self.assertIsNotNone(result) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/command_tests/test_wikipedia_search.py b/tests/command_tests/test_wikipedia_search.py deleted file mode 100644 index e554aab..0000000 --- a/tests/command_tests/test_wikipedia_search.py +++ /dev/null @@ -1,33 +0,0 @@ -import unittest -from commands.wikipedia_search import WikipediaSearchCommand - - -class TestWikipediaSearchCommand(unittest.TestCase): - def test_empty_input(self): - command = WikipediaSearchCommand() - result = command.execute('') - - self.assertIsNone(result) - - def test_garbage_input(self): - command = WikipediaSearchCommand() - result = command.execute('asdhbhklasdvfbuioasvfbhuilasdvfbhjkl') - - self.assertIsNone(result) - - def test_valid_input(self): - command = WikipediaSearchCommand() - result = command.execute("python") - - self.assertIsNotNone(result) - - def test_possible_matches_search(self): - command = WikipediaSearchCommand() - result = command.execute("nuggets") - - self.assertIsNotNone(result) - self.assertFalse(" may refer to " in result['text']) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/model_tests/__init__.py b/tests/model_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/model_tests/test_karma.py b/tests/model_tests/test_karma.py deleted file mode 100644 index 7db23e3..0000000 --- a/tests/model_tests/test_karma.py +++ /dev/null @@ -1,266 +0,0 @@ -import unittest -from datetime import datetime, timedelta - -from pymodm.errors import ValidationError - -from config import Config -from model.karma import Karma, KarmaType - -test_awarder = "default awarder" -test_recipient = "default recipient" - - -def create_karma(awarded_by_username=test_awarder, - awarded_to_username=test_recipient, - reason=Karma.default_reason, - awarded=datetime.today(), - karma_type=KarmaType.POZZYPOZ): - k = Karma(awarded_by_username=awarded_by_username, - awarded_to_username=awarded_to_username, - reason=reason, - awarded=awarded, - karma_type=karma_type) - return k - - -def default_karma_list(): - """Returns a list of 4 karma entries, 2 negative, 2 positive""" - return list([create_karma(karma_type=KarmaType.NEGGYNEG), create_karma(karma_type=KarmaType.NEGGYNEG), - create_karma(karma_type=KarmaType.POZZYPOZ), create_karma(karma_type=KarmaType.POZZYPOZ)]) - -def default_leader_board(): - newly_awarded = datetime.today() - recently_awarded = datetime.today() - timedelta(days=5) - award_ages_ago = datetime.today() - timedelta(days=31) - cool_guy = "cool guy" - guy_1 = "guy 1" - guy_2 = "guy 2" - loser = "loser" - default_leader_board = list([create_karma(awarded=newly_awarded, awarded_to_username=cool_guy), - create_karma(awarded=recently_awarded, awarded_to_username=cool_guy.upper()), - create_karma(awarded=recently_awarded, awarded_to_username=cool_guy), - create_karma(awarded=recently_awarded, awarded_to_username=guy_1), - create_karma(awarded=recently_awarded, awarded_to_username=guy_1), - create_karma(awarded=recently_awarded, awarded_to_username=guy_2), - create_karma(awarded=recently_awarded, awarded_to_username=loser, - karma_type=KarmaType.NEGGYNEG), - create_karma(awarded=award_ages_ago)]) - return default_leader_board - -class TestKarma(unittest.TestCase): - def setUp(self): - Config().connect_to_db() - old_karma = Karma.objects.all() - for ok in old_karma: - ok.delete() - - def persist_leaderboard(self): - karma_leader_board = default_leader_board() - for k in karma_leader_board: - k.save() - - return karma_leader_board - - def test_persistence(self): - # Arrange - awarded_to = "testUser1" - awarded_by = "testUser2" - test_reason = "testReason" - test_awarded = datetime.today() - test_type = str(KarmaType.POZZYPOZ) - k = Karma(awarded_to_username=awarded_to, - reason=test_reason, - awarded_by_username=awarded_by, - awarded=test_awarded, - karma_type=test_type) - - # Act - k.save() - - # Assert - loaded_karma = Karma.objects.all()[0] - self.assertEqual(k.awarded_to_username, loaded_karma.awarded_to_username) - self.assertEqual(k.reason, loaded_karma.reason) - self.assertEqual(k.awarded_by_username, loaded_karma.awarded_by_username) - self.assertEqual(k.karma_type, loaded_karma.karma_type) - # We'll assume that awarded is equal cause date equality assertions seem to be guff. - - def test_get_current_net_karma_unknown_recipient_is_zero(self): - recipient = "bfvhdsukvhksdbv" - net_karma = Karma.get_current_net_karma_for_recipient(recipient) - - self.assertEqual(0, net_karma) - - def test_get_leader_board(self): - # Arrange - self.persist_leaderboard() - - expected = [{"username": "cool guy", "net_karma": 3}, - {"username": "guy 1", "net_karma": 2}, - {"username": "guy 2", "net_karma": 1}] - - # Act - leader_board = Karma.get_leader_board() - - # Assert - self.assertEqual(expected, leader_board) - - def test_get_loser_board(self): - # Arrange - self.persist_leaderboard() - - expected = [{"username": "loser", "net_karma": -1}, - {"username": "guy 2", "net_karma": 1}, - {"username": "guy 1", "net_karma": 2}] - - # Act - loser_board = Karma.get_loser_board() - - # Assert - self.assertEqual(expected, loser_board) - - def test_get_current_karma_with_expired(self): - # Arrange - newly_awarded = datetime.today() - award_ages_ago = datetime.today() - timedelta(days=31) - new_karma = create_karma(awarded=newly_awarded) - old_karma = create_karma(awarded=award_ages_ago) - new_karma.save() - old_karma.save() - - # Act - current_karma = Karma.get_current_net_karma_for_recipient(test_recipient) - - # Assert - self.assertEqual(1, current_karma) - - def test_get_current_karma_with_net(self): - # Arrange - karma_to_save = default_karma_list() - for k in karma_to_save: - k.save() - - # Act - net_karma = Karma.get_current_net_karma_for_recipient(test_recipient) - - # Assert - self.assertEqual(0, net_karma) - - def test_get_karma_reasons_all_default(self): - # Arrange - for k in default_karma_list(): - k.save() - - # Act - karma_reasons = Karma.get_current_karma_reasons_for_recipient(test_recipient) - - # Assert - self.assertEqual(len(default_karma_list()), karma_reasons['reasonless']) - self.assertEqual(0, len(karma_reasons['reasoned'])) - - def test_get_karma_reasons_one_non_default(self): - # Arrange - karma_list = default_karma_list() - karma_with_reason = create_karma(reason="This is a silly reason") - karma_list.append(karma_with_reason) - - for k in karma_list: - k.save() - - # Act - karma_reasons = Karma.get_current_karma_reasons_for_recipient(test_recipient) - - # Assert - self.assertEqual(len(default_karma_list()), karma_reasons['reasonless']) - self.assertEqual(list([karma_with_reason]), karma_reasons['reasoned']) - - def test_positive_karma_cannot_be_self_awarded(self): - awarded_to = "testUser1" - awarded_by = "testUser1" - test_reason = "testReason" - test_awarded = datetime.today() - test_type = str(KarmaType.POZZYPOZ) - k = Karma(awarded_to_username=awarded_to, - reason=test_reason, - awarded_by_username=awarded_by, - awarded=test_awarded, - karma_type=test_type) - - self.assertRaises(ValidationError, k.save) - - def test_positive_karma_can_be_awarded_by_others(self): - awarded_to = "testUser1" - awarded_by = "testUser2" - test_reason = "testReason" - test_awarded = datetime.today() - test_type = str(KarmaType.POZZYPOZ) - k = Karma(awarded_to_username=awarded_to, - reason=test_reason, - awarded_by_username=awarded_by, - awarded=test_awarded, - karma_type=test_type) - k.save() - - def test_negative_karma_can_be_self_awarded(self): - awarded_to = "testUser1" - awarded_by = "testUser1" - test_reason = "testReason" - test_awarded = datetime.today() - test_type = str(KarmaType.NEGGYNEG) - k = Karma(awarded_to_username=awarded_to, - reason=test_reason, - awarded_by_username=awarded_by, - awarded=test_awarded, - karma_type=test_type) - k.save() - - def test_negative_karma_can_be_awarded_by_others(self): - awarded_to = "testUser1" - awarded_by = "testUser2" - test_reason = "testReason" - test_awarded = datetime.today() - test_type = str(KarmaType.NEGGYNEG) - k = Karma(awarded_to_username=awarded_to, - reason=test_reason, - awarded_by_username=awarded_by, - awarded=test_awarded, - karma_type=test_type) - k.save() - - def test_karma_calculation_bug(self): - toms_karma = list([Karma(awarded_to_username="TomAllen", reason="", awarded_by_username="gee", - awarded=datetime.now(), karma_type=KarmaType.NEGGYNEG), - Karma(awarded_to_username="TomAllen", reason="", awarded_by_username="gee", - awarded=datetime.now(), karma_type=KarmaType.NEGGYNEG), - Karma(awarded_to_username="TomAllen", reason="", awarded_by_username="gee", - awarded=datetime.now(), karma_type=KarmaType.NEGGYNEG)]) - - for k in toms_karma: - k.save() - - net_karma = Karma.get_current_net_karma_for_recipient("tomallen") - - self.assertEqual(-3, net_karma) - - def test_karma_reasons_do_not_filter_on_substrings(self): - # Arrange - dan_karma = Karma(awarded_to_username="dan", reason="a reason", awarded_by_username=test_awarder, - awarded=datetime.now(), karma_type=KarmaType.POZZYPOZ) - dan_karma.save() - - seniordaniel_karma = Karma(awarded_to_username="seniordaniel", reason="a different reason", - awarded_by_username=test_awarder, awarded=datetime.now(), - karma_type=KarmaType.POZZYPOZ) - seniordaniel_karma.save() - - expected_reasoned_karma = list([dan_karma]) - - # Act - dan_reasons = Karma.get_current_karma_reasons_for_recipient("dan") - - # Assert - self.assertEqual(expected_reasoned_karma, dan_reasons["reasoned"]) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/model_tests/test_member.py b/tests/model_tests/test_member.py deleted file mode 100644 index 9a30ff1..0000000 --- a/tests/model_tests/test_member.py +++ /dev/null @@ -1,46 +0,0 @@ -import unittest -from model.member import Member -from model.karma import Karma, KarmaType -from config import Config -from unittest.mock import patch - - -class TestMember(unittest.TestCase): - test_username = "Persistence test user" - - def test_persistence(self): - # Arrange - Config().connect_to_db() - m = Member(TestMember.test_username) - - # Act - m.save() - - # Assert - retrieved_members = Member.objects.raw({'_id':TestMember.test_username}) - self.assertEqual(1, retrieved_members.count()) - self.assertEqual(m, retrieved_members[0]) - - # Tear down - m.delete() - - def test_get_current_karma(self): - with patch.object(Karma, "get_current_net_karma_for_recipient") as test_method: - m = Member(TestMember.test_username) - - m.get_current_karma() - - test_method.assert_called_once_with(TestMember.test_username) - - - def test_get_karma_reasons(self): - with patch.object(Karma, "get_current_karma_reasons_for_recipient") as test_method: - m = Member(TestMember.test_username) - - m.get_karma_reasons() - - test_method.assert_called_once_with(TestMember.test_username) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/slack_channel_tests/__init__.py b/tests/slack_channel_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/slack_channel_tests/test_add_karma_reaction_handler.py b/tests/slack_channel_tests/test_add_karma_reaction_handler.py deleted file mode 100644 index a67e1d1..0000000 --- a/tests/slack_channel_tests/test_add_karma_reaction_handler.py +++ /dev/null @@ -1,48 +0,0 @@ -# coding=utf-8 -import unittest - -from unittest.mock import patch - -from slack_channel import AddKarmaReactionHandler -from commands import AddKarmaCommand -from model.karma import KarmaType - -valid_event = {'reactor': 'kinduser', 'reaction': 'joy', 'reactee': 'deservinguser'} -valid_event_with_skintone = {'reactor': 'kinduser', 'reaction': '+1::skin-tone-2', 'reactee': 'deservinguser'} -invalid_event = {'reactor': 'kinduser', 'reaction': 'robot_face', 'reactee': 'deservinguser'} - -class TestAddKarmaReactionHandler(unittest.TestCase): - - def test_parses_underlying_reaction_when_skintone_applied(self): - handler = AddKarmaReactionHandler() - raw_reaction = '+1::skin-tone-2' - reaction = handler.parse_reaction(raw_reaction) - self.assertEqual(reaction, '+1') - - def test_handles_valid_event(self): - handler = AddKarmaReactionHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_handles_valid_event_with_skintone(self): - handler = AddKarmaReactionHandler() - can_handle = handler.can_handle(valid_event_with_skintone) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self): - handler = AddKarmaReactionHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(AddKarmaReactionHandler, "_send_reaction_response") - @patch.object(AddKarmaCommand, "execute") - def test_command_executes_and_response_sent(self, execute_method, response_method): - handler = AddKarmaReactionHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with( - awarded_to=valid_event["reactee"], - awarded_by=valid_event["reactor"], - reason="Reacted with "+valid_event["reaction"], - karma_type=KarmaType.POZZYPOZ - ) - response_method.assert_called_once_with(valid_event) \ No newline at end of file diff --git a/tests/slack_channel_tests/test_cocktail_of_the_week_event_handler.py b/tests/slack_channel_tests/test_cocktail_of_the_week_event_handler.py deleted file mode 100644 index be9a1a5..0000000 --- a/tests/slack_channel_tests/test_cocktail_of_the_week_event_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import CocktailOfTheWeekSearchCommand -from config import Config -from slack_channel import CocktailOfTheWeekEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "cotw"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "cotw -?"} -cotw_response = "Water. (£0.00). Water" - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestCocktailOfTheWeekEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = CocktailOfTheWeekEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = CocktailOfTheWeekEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(CocktailOfTheWeekEventHandler, "_send_message_response") - @patch.object(CocktailOfTheWeekSearchCommand, "execute", return_value=cotw_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = CocktailOfTheWeekEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with() - - @patch.object(CocktailOfTheWeekSearchCommand, "execute", return_value=cotw_response) - @patch.object(CocktailOfTheWeekEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = CocktailOfTheWeekEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with(cotw_response, valid_event) - - @patch.object(CocktailOfTheWeekSearchCommand, "execute", return_value=None) - @patch.object(CocktailOfTheWeekEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = CocktailOfTheWeekEventHandler() - handler.handle(valid_event) - empty_results_message = "You're on your own I'm afraid - https://thebrassmonkeygla.co.uk/." - response_method.assert_called_once_with(empty_results_message, valid_event) - - @patch.object(CocktailOfTheWeekSearchCommand, "get_purpose", return_value="Cocktail of the Week") - @patch.object(CocktailOfTheWeekEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = CocktailOfTheWeekEventHandler() - handler.handle(help_event) - expected_help = "Cocktail of the week lookup from the Brass Monkey" + os.linesep + \ - "Cocktail of the Week" + os.linesep + \ - "Usage: `" + test_prefix + "cotw " + "`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - - if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_decrement_karma_event_handler.py b/tests/slack_channel_tests/test_decrement_karma_event_handler.py deleted file mode 100644 index 8f48120..0000000 --- a/tests/slack_channel_tests/test_decrement_karma_event_handler.py +++ /dev/null @@ -1,72 +0,0 @@ -# coding=utf-8 -import unittest - -from unittest.mock import patch - -import os - -from commands import AddKarmaCommand -from model.karma import KarmaType -from slack_channel import DecrementKarmaEventHandler - -valid_event = {"text": "-- username for being awful", "user": "meanuser"} -valid_event_no_space = {"text": "--username for being awful", "user": "meanuser"} - -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": "-- -?"} - - -class TestDecrementKarmaEventHandler(unittest.TestCase): - - def test_handles_correct_event(self): - handler = DecrementKarmaEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self): - handler = DecrementKarmaEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(DecrementKarmaEventHandler, "_send_reaction_response") - @patch.object(AddKarmaCommand, "execute") - def test_command_executes_and_response_sent(self, execute_method, response_method): - handler = DecrementKarmaEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with( - awarded_to="username", - awarded_by=valid_event["user"], - reason="being awful", - karma_type=KarmaType.NEGGYNEG - ) - response_method.assert_called_once_with(valid_event) - - @patch.object(DecrementKarmaEventHandler, "_send_reaction_response") - @patch.object(AddKarmaCommand, "execute") - def test_command_executes_and_response_sent_nospace(self, execute_method, response_method): - handler = DecrementKarmaEventHandler() - handler.handle(valid_event_no_space) - execute_method.assert_called_once_with( - awarded_to="username", - awarded_by=valid_event["user"], - reason="being awful", - karma_type=KarmaType.NEGGYNEG - ) - response_method.assert_called_once_with(valid_event_no_space) - - @patch.object(AddKarmaCommand, "get_purpose", return_value="Karmas") - @patch.object(DecrementKarmaEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method): - handler = DecrementKarmaEventHandler() - handler.handle(help_event) - expected_help = "Neggy-neg" + os.linesep + "Karmas" + os.linesep +\ - "Usage: `-- recipient [[for ] reason]`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - - - - - - if __name__ == '__main__': - unittest.main() diff --git a/tests/slack_channel_tests/test_get_current_karma_reasons_event_handler.py b/tests/slack_channel_tests/test_get_current_karma_reasons_event_handler.py deleted file mode 100644 index afb4b11..0000000 --- a/tests/slack_channel_tests/test_get_current_karma_reasons_event_handler.py +++ /dev/null @@ -1,110 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GetCurrentKarmaReasonsCommand -from config import Config -from model.karma import Karma, KarmaType -from model.member import Member -from slack_channel import GetCurrentKarmaReasonsEventHandler - -test_prefix = "_" -valid_event_plain_recipient = {"text": test_prefix + "reasons recipient", "user": "username"} -valid_event_slack_id_recipient = {"text": test_prefix + "reasons <@slack_id>", "user": "username"} -valid_event_slack_id_recipient_trailing_stuff = {"text": test_prefix + "reasons <@slack_id> some extra guff", "user": "username"} -valid_event_no_recipient = {"text": test_prefix + "reasons", "user": "username"} -invalid_event = {'text': '_g test'} -fake_member = Member(username="member_name", slack_id="slack_id") -help_event = {"text": test_prefix + "reasons -?"} -command_result = {"reasonless": 10, "reasoned": list([Karma(reason="reason1", karma_type=KarmaType.POZZYPOZ, awarded_by_username="user1"), - Karma(reason="reason2", karma_type=KarmaType.POZZYPOZ, awarded_by_username="user2"), - Karma(reason="reason3", karma_type=KarmaType.NEGGYNEG, awarded_by_username="user3")])} -expected_response = "Recent Karma for recipient:" + os.linesep + \ - "Recently awarded with no reason: 10." + os.linesep + \ - '++ from user1 for "reason1"' + os.linesep + \ - '++ from user2 for "reason2"' + os.linesep + \ - '-- from user3 for "reason3"' - - -@patch.object(Config, "connect_to_db") -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGetCurrentKarmaReasonsEventHandler(unittest.TestCase): - - def test_handles_correct_event_with_plain_recipient(self, prefix_method, config_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - can_handle = handler.can_handle(valid_event_plain_recipient) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_slack_id_recipient(self, prefix_method, config_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - can_handle = handler.can_handle(valid_event_slack_id_recipient) - self.assertTrue(can_handle) - - def test_handles_correct_event_no_recipient(self, prefix_method, config_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - can_handle = handler.can_handle(valid_event_no_recipient) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GetCurrentKarmaReasonsEventHandler, "_send_dm_response") - @patch.object(GetCurrentKarmaReasonsCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_plain_recipient(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - handler.handle(valid_event_plain_recipient) - execute_method.assert_called_once_with("recipient") - - @patch.object(GetCurrentKarmaReasonsEventHandler, "_send_dm_response") - @patch.object(GetCurrentKarmaReasonsCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_slack_id_recipient(self, execute_method, response_method, config_method, - prefix_method, db_method): - with patch.object(Member, "get_member_by_slack_id", return_value=fake_member): - handler = GetCurrentKarmaReasonsEventHandler() - handler.handle(valid_event_slack_id_recipient) - execute_method.assert_called_once_with(fake_member.username)\ - - @patch.object(GetCurrentKarmaReasonsEventHandler, "_send_dm_response") - @patch.object(GetCurrentKarmaReasonsCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_slack_id_and_guff_recipient(self, execute_method, response_method, - config_method, prefix_method, db_method): - with patch.object(Member, "get_member_by_slack_id", return_value=fake_member): - handler = GetCurrentKarmaReasonsEventHandler() - handler.handle(valid_event_slack_id_recipient_trailing_stuff) - execute_method.assert_called_once_with(fake_member.username) - - @patch.object(GetCurrentKarmaReasonsEventHandler, "_send_dm_response") - @patch.object(GetCurrentKarmaReasonsCommand, "execute", return_value=command_result) - def test_command_execute_is_called_no_recipient(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - handler.handle(valid_event_no_recipient) - execute_method.assert_called_once_with(valid_event_no_recipient["user"]) - - @patch.object(GetCurrentKarmaReasonsCommand, "execute", return_value=command_result) - @patch.object(GetCurrentKarmaReasonsEventHandler, "_send_dm_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, - config_method, prefix_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - handler.handle(valid_event_plain_recipient) - response_method.assert_called_once_with(expected_response, valid_event_plain_recipient) - - @patch.object(GetCurrentKarmaReasonsCommand, "get_purpose", return_value="GetCurrentKarmaReasons") - @patch.object(GetCurrentKarmaReasonsEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method, db_method): - handler = GetCurrentKarmaReasonsEventHandler() - handler.handle(help_event) - expected_help = "Karma Reasons" + os.linesep + \ - "GetCurrentKarmaReasons"+ os.linesep +"Usage: `" + \ - test_prefix + "reasons [recipient ]`" - response_method.assert_called_once_with(expected_help, help_event) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_get_current_net_karma_event_handler.py b/tests/slack_channel_tests/test_get_current_net_karma_event_handler.py deleted file mode 100644 index 004e706..0000000 --- a/tests/slack_channel_tests/test_get_current_net_karma_event_handler.py +++ /dev/null @@ -1,121 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GetCurrentNetKarmaCommand -from config import Config -from model.member import Member -from slack_channel import GetCurrentNetKarmaEventHandler - -test_prefix = "_" -valid_event_plain_recipient = {"text": test_prefix + "karma recipient", "user": "username"} -valid_event_slack_id_recipient = {"text": test_prefix + "karma <@slack_id>", "user": "username"} -valid_event_slack_id_recipient_trailing_stuff = {"text": test_prefix + "karma <@slack_id> some extra guff", "user": "username"} -valid_event_no_recipient = {"text": test_prefix + "karma", "user": "username"} -invalid_event = {'text': '_g test',} -fake_member = Member(username="member_name", slack_id="slack_id") -help_event = {"text": test_prefix + "karma -?"} - - -@patch.object(Config, "connect_to_db") -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGetCurrentNetKarmaEventHandler(unittest.TestCase): - - def test_handles_correct_event_with_plain_recipient(self, prefix_method, config_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - can_handle = handler.can_handle(valid_event_plain_recipient) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_slack_id_recipient(self, prefix_method, config_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - can_handle = handler.can_handle(valid_event_slack_id_recipient) - self.assertTrue(can_handle) - - def test_handles_correct_event_no_recipient(self, prefix_method, config_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - can_handle = handler.can_handle(valid_event_no_recipient) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value = 0) - def test_command_execute_is_called_with_plain_recipient(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_plain_recipient) - execute_method.assert_called_once_with("recipient") - - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value=0) - def test_command_execute_is_called_with_slack_id_recipient(self, execute_method, response_method, config_method, - prefix_method, db_method): - with patch.object(Member, "get_member_by_slack_id", return_value=fake_member): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_slack_id_recipient) - execute_method.assert_called_once_with(fake_member.username)\ - - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value=0) - def test_command_execute_is_called_with_slack_id_andguff_recipient(self, execute_method, response_method, - config_method, prefix_method, db_method): - with patch.object(Member, "get_member_by_slack_id", return_value=fake_member): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_slack_id_recipient_trailing_stuff) - execute_method.assert_called_once_with(fake_member.username) - - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value=0) - def test_command_execute_is_called_no_recipient(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_no_recipient) - execute_method.assert_called_once_with(valid_event_no_recipient["user"]) - - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value=0) - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - def test_command_result_is_correctly_built_with_plain_recipient(self, response_method, execute_method, - config_method, prefix_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_plain_recipient) - response_method.assert_called_once_with("recipient: 0", valid_event_plain_recipient) - - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value=0) - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - def test_command_result_is_correctly_built_with_slack_id_recipient(self, response_method, execute_method, - config_method, prefix_method, db_method): - with patch.object(Member, "get_member_by_slack_id", return_value=fake_member): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_slack_id_recipient) - response_method.assert_called_once_with(fake_member.username + ": 0", valid_event_slack_id_recipient) - - @patch.object(GetCurrentNetKarmaCommand, "execute", return_value=0) - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - def test_command_result_is_correctly_built_with_slack_id_recipient_trailing_stuff(self, response_method, - execute_method, config_method, - prefix_method, db_method): - with patch.object(Member, "get_member_by_slack_id", return_value=fake_member): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(valid_event_slack_id_recipient_trailing_stuff) - response_method.assert_called_once_with(fake_member.username + ": 0", - valid_event_slack_id_recipient_trailing_stuff) - - @patch.object(GetCurrentNetKarmaCommand, "get_purpose", return_value="GetCurrentNetKarma") - @patch.object(GetCurrentNetKarmaEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method, db_method): - handler = GetCurrentNetKarmaEventHandler() - handler.handle(help_event) - expected_help = "Get Current Karma" + os.linesep + \ - "GetCurrentNetKarma"+ os.linesep +\ - "Usage: `" + test_prefix + "karma [recipient ]`" +\ - handler.get_usage_addendum() - response_method.assert_called_once_with(expected_help, help_event) - - if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_get_leader_board_event_handler.py b/tests/slack_channel_tests/test_get_leader_board_event_handler.py deleted file mode 100644 index 4192b83..0000000 --- a/tests/slack_channel_tests/test_get_leader_board_event_handler.py +++ /dev/null @@ -1,110 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GetLeaderBoardCommand -from config import Config -from model.member import Member -from slack_channel import GetLeaderBoardEventHandler - -test_prefix = "_" -valid_event_no_size = {"text": test_prefix + "leaderboard"} -valid_event_size_10 = {"text": test_prefix + "leaderboard 10"} -valid_event_guff_size= {"text": test_prefix + "leaderboard -1"} -valid_event_size_0= {"text": test_prefix + "leaderboard 0"} -invalid_event = {'text': '_g test'} -fake_member = Member(username="member_name", slack_id="slack_id") -help_event = {"text": test_prefix + "leaderboard -?"} -command_result = list([{"username": "cool guy", "net_karma": 1}, - {"username": "guy", "net_karma": 0}, - {"username": "loser", "net_karma": -1}]) -expected_response = "cool guy: 1" + os.linesep + \ - "guy: 0" + os.linesep + \ - "loser: -1" - - -@patch.object(Config, "connect_to_db") -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGetLeaderBoardEventHandler(unittest.TestCase): - - def test_handles_correct_event_no_size(self, prefix_method, config_method, db_method): - handler = GetLeaderBoardEventHandler() - can_handle = handler.can_handle(valid_event_no_size) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_size(self, prefix_method, config_method, db_method): - handler = GetLeaderBoardEventHandler() - can_handle = handler.can_handle(valid_event_size_10) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_guff_size(self, prefix_method, config_method, db_method): - handler = GetLeaderBoardEventHandler() - can_handle = handler.can_handle(valid_event_guff_size) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_size_0(self, prefix_method, config_method, db_method): - handler = GetLeaderBoardEventHandler() - can_handle = handler.can_handle(valid_event_size_0) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method, db_method): - handler = GetLeaderBoardEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GetLeaderBoardEventHandler, "_send_message_response") - @patch.object(GetLeaderBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_no_size(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLeaderBoardEventHandler() - handler.handle(valid_event_no_size) - execute_method.assert_called_once_with(3) - - @patch.object(GetLeaderBoardEventHandler, "_send_message_response") - @patch.object(GetLeaderBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_size(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLeaderBoardEventHandler() - handler.handle(valid_event_size_10) - execute_method.assert_called_once_with(10) - - @patch.object(GetLeaderBoardEventHandler, "_send_message_response") - @patch.object(GetLeaderBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_guff_size(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLeaderBoardEventHandler() - handler.handle(valid_event_guff_size) - execute_method.assert_called_once_with(3) - - @patch.object(GetLeaderBoardEventHandler, "_send_message_response") - @patch.object(GetLeaderBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_size_0(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLeaderBoardEventHandler() - handler.handle(valid_event_size_0) - execute_method.assert_called_once_with(3) - - @patch.object(GetLeaderBoardCommand, "execute", return_value=command_result) - @patch.object(GetLeaderBoardEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, - prefix_method, db_method): - handler = GetLeaderBoardEventHandler() - handler.handle(valid_event_no_size) - response_method.assert_called_once_with(expected_response, valid_event_no_size) - - @patch.object(GetLeaderBoardCommand, "get_purpose", return_value="GetLeaderBoard") - @patch.object(GetLeaderBoardEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method, db_method): - handler = GetLeaderBoardEventHandler() - handler.handle(help_event) - expected_help = "Get Leaderboard" + os.linesep + \ - "GetLeaderBoard"+ os.linesep +"Usage: `" + \ - test_prefix + "leaderboard [size of leaderboard. Default is 3]`" - response_method.assert_called_once_with(expected_help, help_event) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_get_loser_board_event_handler.py b/tests/slack_channel_tests/test_get_loser_board_event_handler.py deleted file mode 100644 index 44519d4..0000000 --- a/tests/slack_channel_tests/test_get_loser_board_event_handler.py +++ /dev/null @@ -1,110 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GetLoserBoardCommand -from config import Config -from model.member import Member -from slack_channel import GetLoserBoardEventHandler - -test_prefix = "_" -valid_event_no_size = {"text": test_prefix + "loserboard"} -valid_event_size_10 = {"text": test_prefix + "loserboard 10"} -valid_event_guff_size= {"text": test_prefix + "loserboard -1"} -valid_event_size_0= {"text": test_prefix + "loserboard 0"} -invalid_event = {'text': '_g test'} -fake_member = Member(username="member_name", slack_id="slack_id") -help_event = {"text": test_prefix + "loserboard -?"} -command_result = list([{"username": "loser", "net_karma": -1}, - {"username": "guy", "net_karma": 0}, - {"username": "cool guy", "net_karma": 1}]) -expected_response = "loser: -1" + os.linesep + \ - "guy: 0" + os.linesep + \ - "cool guy: 1" - - -@patch.object(Config, "connect_to_db") -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGetLoserBoardEventHandler(unittest.TestCase): - - def test_handles_correct_event_no_size(self, prefix_method, config_method, db_method): - handler = GetLoserBoardEventHandler() - can_handle = handler.can_handle(valid_event_no_size) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_size(self, prefix_method, config_method, db_method): - handler = GetLoserBoardEventHandler() - can_handle = handler.can_handle(valid_event_size_10) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_guff_size(self, prefix_method, config_method, db_method): - handler = GetLoserBoardEventHandler() - can_handle = handler.can_handle(valid_event_guff_size) - self.assertTrue(can_handle) - - def test_handles_correct_event_with_size_0(self, prefix_method, config_method, db_method): - handler = GetLoserBoardEventHandler() - can_handle = handler.can_handle(valid_event_size_0) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method, db_method): - handler = GetLoserBoardEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GetLoserBoardEventHandler, "_send_message_response") - @patch.object(GetLoserBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_no_size(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLoserBoardEventHandler() - handler.handle(valid_event_no_size) - execute_method.assert_called_once_with(3) - - @patch.object(GetLoserBoardEventHandler, "_send_message_response") - @patch.object(GetLoserBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_size(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLoserBoardEventHandler() - handler.handle(valid_event_size_10) - execute_method.assert_called_once_with(10) - - @patch.object(GetLoserBoardEventHandler, "_send_message_response") - @patch.object(GetLoserBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_guff_size(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLoserBoardEventHandler() - handler.handle(valid_event_guff_size) - execute_method.assert_called_once_with(3) - - @patch.object(GetLoserBoardEventHandler, "_send_message_response") - @patch.object(GetLoserBoardCommand, "execute", return_value=command_result) - def test_command_execute_is_called_with_size_0(self, execute_method, response_method, config_method, - prefix_method, db_method): - handler = GetLoserBoardEventHandler() - handler.handle(valid_event_size_0) - execute_method.assert_called_once_with(3) - - @patch.object(GetLoserBoardCommand, "execute", return_value=command_result) - @patch.object(GetLoserBoardEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, - prefix_method, db_method): - handler = GetLoserBoardEventHandler() - handler.handle(valid_event_no_size) - response_method.assert_called_once_with(expected_response, valid_event_no_size) - - @patch.object(GetLoserBoardCommand, "get_purpose", return_value="GetLoserBoard") - @patch.object(GetLoserBoardEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method, db_method): - handler = GetLoserBoardEventHandler() - handler.handle(help_event) - expected_help = "Get Loserboard" + os.linesep + \ - "GetLoserBoard"+ os.linesep +"Usage: `" + \ - test_prefix + "loserboard [size of loserboard. Default is 3]`" - response_method.assert_called_once_with(expected_help, help_event) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_giphy_event_handler.py b/tests/slack_channel_tests/test_giphy_event_handler.py deleted file mode 100644 index b793fcc..0000000 --- a/tests/slack_channel_tests/test_giphy_event_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GiphyTranslateCommand -from config import Config -from slack_channel import GiphyEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "gif a valid Giphy command"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "gif -?"} -giphy_response = "https://www.giphy.com/a.gif" - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGiphyEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = GiphyEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = GiphyEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GiphyEventHandler, "_send_message_response") - @patch.object(GiphyTranslateCommand, "execute", return_value = giphy_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = GiphyEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with(valid_event["text"][5:]) - - @patch.object(GiphyTranslateCommand, "execute", return_value=giphy_response) - @patch.object(GiphyEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = GiphyEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with(giphy_response, valid_event) - - @patch.object(GiphyTranslateCommand, "execute", return_value=None) - @patch.object(GiphyEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = GiphyEventHandler() - handler.handle(valid_event) - empty_results_message = "No gifs found for \"" + valid_event["text"][5:] + "\"" - response_method.assert_called_once_with(empty_results_message, valid_event) - - @patch.object(GiphyTranslateCommand, "get_purpose", return_value="Giphys") - @patch.object(GiphyEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = GiphyEventHandler() - handler.handle(help_event) - expected_help = "Giphy" + os.linesep + \ - "Giphys" + os.linesep + \ - "Usage: `" + test_prefix + "gif " + "`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_google_event_handler.py b/tests/slack_channel_tests/test_google_event_handler.py deleted file mode 100644 index 2db5df9..0000000 --- a/tests/slack_channel_tests/test_google_event_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GoogleSearchCommand -from config import Config -from slack_channel import GoogleEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "g a valid google command"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "g -?"} -google_response = {"title": "response title", "link": "response_link"} - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGoogleEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = GoogleEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = GoogleEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GoogleEventHandler, "_send_message_response") - @patch.object(GoogleSearchCommand, "execute", return_value = google_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = GoogleEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with(valid_event["text"][3:]) - - @patch.object(GoogleSearchCommand, "execute", return_value=google_response) - @patch.object(GoogleEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = GoogleEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with("response title response_link", valid_event) - - @patch.object(GoogleSearchCommand, "execute", return_value=None) - @patch.object(GoogleEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = GoogleEventHandler() - handler.handle(valid_event) - empty_results_message = "No results found for \""+ valid_event["text"][3:]+"\"" - response_method.assert_called_once_with(empty_results_message, valid_event) - - @patch.object(GoogleSearchCommand, "get_purpose", return_value="Googles") - @patch.object(GoogleEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = GoogleEventHandler() - handler.handle(help_event) - expected_help = "Google" + os.linesep + \ - "Googles"+ os.linesep +\ - "Usage: `" + test_prefix + "g " + "`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/slack_channel_tests/test_google_image_event_handler.py b/tests/slack_channel_tests/test_google_image_event_handler.py deleted file mode 100644 index 08aa5e6..0000000 --- a/tests/slack_channel_tests/test_google_image_event_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import GoogleImageSearchCommand -from config import Config -from slack_channel import GoogleImageEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "gi a valid google command"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "gi -?"} -google_response = "http://www.test.com/someimage.jpg" - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestGoogleEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = GoogleImageEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = GoogleImageEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(GoogleImageEventHandler, "_send_message_response") - @patch.object(GoogleImageSearchCommand, "execute", return_value = google_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = GoogleImageEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with(valid_event["text"][4:]) - - @patch.object(GoogleImageSearchCommand, "execute", return_value=google_response) - @patch.object(GoogleImageEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = GoogleImageEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with(google_response, valid_event) - - @patch.object(GoogleImageSearchCommand, "execute", return_value=None) - @patch.object(GoogleImageEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = GoogleImageEventHandler() - handler.handle(valid_event) - empty_results_message = "No results found for \""+ valid_event["text"][4:]+"\"" - response_method.assert_called_once_with(empty_results_message, valid_event) - - @patch.object(GoogleImageSearchCommand, "get_purpose", return_value="Googles") - @patch.object(GoogleImageEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = GoogleImageEventHandler() - handler.handle(help_event) - expected_help = "Google Image" + os.linesep + \ - "Googles"+ os.linesep +\ - "Usage: `" + test_prefix + "gi " + "`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/slack_channel_tests/test_help_event_handler.py b/tests/slack_channel_tests/test_help_event_handler.py deleted file mode 100644 index 6562d28..0000000 --- a/tests/slack_channel_tests/test_help_event_handler.py +++ /dev/null @@ -1,63 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock - -import os - -from config import Config -from slack_channel import HelpEventHandler -from slack_channel.abstract_event_handler import AbstractEventHandler - -test_prefix = "_" -valid_events = [{"text": test_prefix + "help"},{"text": test_prefix + "?"},{"text": test_prefix + "list"}] -invalid_event = {"text": "dnfjklsnjfsdl"} -help_event = {"text": "_help -?"} - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestHelpEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = HelpEventHandler() - for ve in valid_events: - can_handle = handler.can_handle(ve) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = HelpEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(HelpEventHandler, "_send_message_response") - def test_get_usage(self, response_method, config_method, prefix_method): - handler = HelpEventHandler() - handler.handle(help_event) - expected_help = "Help" + os.linesep + \ - "Usage: `"+ test_prefix + "help` or `" + test_prefix + "?` or `" + test_prefix + "list`" - response_method.assert_called_once_with(expected_help, help_event) - - def test_execution(self, config_method, prefix_method): - message1 = "message 1" - message2 = "message 2" - message3 = "message 3" - - mock_handler_1 = MagicMock(AbstractEventHandler) - mock_handler_1.build_help_message.return_value = message1 - mock_handler_2 = MagicMock(AbstractEventHandler) - mock_handler_2.build_help_message.return_value = message2 - mock_handler_3 = MagicMock(AbstractEventHandler) - mock_handler_3.build_help_message.return_value = message3 - - mock_handlers = list([mock_handler_1, mock_handler_2, mock_handler_3]) - - event = valid_events[0] - - with patch.object(HelpEventHandler, "_send_dm_response") as response_method: - handler = HelpEventHandler(command_handlers=mock_handlers) - handler.handle(event) - - expected_message_text = message1 + os.linesep + message2 + os.linesep + message3 - - response_method.assert_called_once_with(expected_message_text, event) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_increment_karma_event_handler.py b/tests/slack_channel_tests/test_increment_karma_event_handler.py deleted file mode 100644 index 338a52e..0000000 --- a/tests/slack_channel_tests/test_increment_karma_event_handler.py +++ /dev/null @@ -1,68 +0,0 @@ -# coding=utf-8 -import unittest - -from unittest.mock import patch - -import os - -from commands import AddKarmaCommand -from model.karma import KarmaType -from slack_channel import IncrementKarmaEventHandler - -valid_event = {"text": "++ username for being awesome", "user": "kinduser"} -valid_event_no_space = {"text": "++username for being awesome", "user": "kinduser"} - -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": "++ -?"} - -class TestIncrementKarmaEventHandler(unittest.TestCase): - - def test_handles_correct_event(self): - handler = IncrementKarmaEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self): - handler = IncrementKarmaEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(IncrementKarmaEventHandler, "_send_reaction_response") - @patch.object(AddKarmaCommand, "execute") - def test_command_executes_and_response_sent(self, execute_method, response_method): - handler = IncrementKarmaEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with( - awarded_to="username", - awarded_by=valid_event["user"], - reason="being awesome", - karma_type=KarmaType.POZZYPOZ - ) - response_method.assert_called_once_with(valid_event) - - @patch.object(AddKarmaCommand, "get_purpose", return_value="Karmas") - @patch.object(IncrementKarmaEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method): - handler = IncrementKarmaEventHandler() - handler.handle(help_event) - expected_help = "Pozzy-poz" + os.linesep + \ - "Karmas" + os.linesep +\ - "Usage: `++ recipient [[for ] reason]`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - - @patch.object(IncrementKarmaEventHandler, "_send_reaction_response") - @patch.object(AddKarmaCommand, "execute") - def test_command_executes_and_response_sent_nospace(self, execute_method, response_method): - handler = IncrementKarmaEventHandler() - handler.handle(valid_event_no_space) - execute_method.assert_called_once_with( - awarded_to="username", - awarded_by=valid_event["user"], - reason="being awesome", - karma_type=KarmaType.POZZYPOZ - ) - response_method.assert_called_once_with(valid_event_no_space) - - if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_karma_handler_command_parsing.py b/tests/slack_channel_tests/test_karma_handler_command_parsing.py deleted file mode 100644 index c4dc8a7..0000000 --- a/tests/slack_channel_tests/test_karma_handler_command_parsing.py +++ /dev/null @@ -1,97 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -from model.karma import KarmaType -from model.member import Member -from slack_channel import IncrementKarmaEventHandler - - -class TestKarmaHandlerCommandParsing(unittest.TestCase): - - handler = IncrementKarmaEventHandler() - - def _check_parsing(self, command_message, expected): - # Act - parsed_command = self.handler._parse_command_text(command_message) - - # Assert - self.assertEqual(expected, parsed_command) - - def test_reasonless_command_one_word_recipient(self): - # Arrange - command_message = "++ recipient" - expected = {"recipient": "recipient", "reason":"", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - def test_reasonless_command_one_word_slack_user_recipient(self): - m = Member(username="recipient", slack_id="slack_id") - with patch.object(Member, "get_member_by_slack_id", return_value=m): - # Arrange - command_message = "++ <@slack_id>" - expected = {"recipient": "recipient", "reason":"", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - @patch.object(IncrementKarmaEventHandler, "_username_is_known", return_value=False) - def test_reasonless_command_multi_word_recipient(self, known_user_method): - # Arrange - command_message = "++ some recipient" - expected = {"recipient": "some recipient", "reason": "", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - def test_reasoned_command_one_word_recipient(self): - # Arrange - command_message = "++ recipient for some reason" - expected = {"recipient": "recipient", "reason": "some reason", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - @patch.object(IncrementKarmaEventHandler, "_username_is_known", return_value=False) - def test_reasoned_command_multi_word_recipient(self, known_user_method): - # Arrange - command_message = "++ some recipient for some reason" - expected = {"recipient": "some recipient", "reason": "some reason", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - @patch.object(IncrementKarmaEventHandler, "_username_is_known", return_value=True) - def test_reasoned_command_user_recipient_no_for(self, known_user_method): - # Arrange - command_message = "++ username is sexy" - expected = {"recipient": "username", "reason": "is sexy", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - def test_reasoned_command_user_recipient_with_for(self): - # Arrange - command_message = "++ username for being sexy" - expected = {"recipient": "username", "reason": "being sexy", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - def test_reasoned_command_slack_id_recipient_with_for(self): - # Arrange - m = Member(username="username", slack_id="slack_id") - with patch.object(Member, "get_member_by_slack_id", return_value=m): - command_message = "++ <@slack_id> for being sexy" - expected = {"recipient": "username", "reason": "being sexy", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) - - def test_reasoned_command_user_recipient_with_multiple_fors(self): - command_message = "++ username for being sexy and for being nice" - expected = {"recipient": "username", "reason": "being sexy and for being nice", "karma_type": KarmaType.POZZYPOZ} - - # Act & Assert - self._check_parsing(command_message, expected) \ No newline at end of file diff --git a/tests/slack_channel_tests/test_release_event_handler.py b/tests/slack_channel_tests/test_release_event_handler.py deleted file mode 100644 index 15ac063..0000000 --- a/tests/slack_channel_tests/test_release_event_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import ReleaseCommand -from config import Config -from slack_channel import ReleaseEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "release"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "release -?"} -release_response = "These are some release notes" - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestReleaseEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = ReleaseEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = ReleaseEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(ReleaseEventHandler, "_send_message_response") - @patch.object(ReleaseCommand, "execute", return_value = release_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = ReleaseEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with() - - @patch.object(ReleaseCommand, "execute", return_value=release_response) - @patch.object(ReleaseEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = ReleaseEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with(release_response, valid_event) - - @patch.object(ReleaseCommand, "execute", return_value=None) - @patch.object(ReleaseEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = ReleaseEventHandler() - handler.handle(valid_event) - empty_results_message = 'Unable to retrieve latest release info.' - response_method.assert_called_once_with(empty_results_message, valid_event) - - @patch.object(ReleaseCommand, "get_purpose", return_value="Releases") - @patch.object(ReleaseEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = ReleaseEventHandler() - handler.handle(help_event) - expected_help = "Release" + os.linesep + \ - "Releases" + os.linesep + \ - "Usage: `_release`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/slack_channel_tests/test_slack_parser.py b/tests/slack_channel_tests/test_slack_parser.py deleted file mode 100644 index 85fa3b2..0000000 --- a/tests/slack_channel_tests/test_slack_parser.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -from unittest.mock import patch - -from model.member import Member -from slack_channel.slack_parser import SlackParser - -parser = SlackParser() - -class TestSlackParser(unittest.TestCase): - - def test_single_word_no_slack_id(self): - expected = "hello" - actual = parser.replace_slack_id_tokens_with_usernames(expected) - - self.assertEqual(expected, actual) - - def test_non_slack_id_not_replaced(self): - expected = "this text has no slack ids" - actual = parser.replace_slack_id_tokens_with_usernames(expected) - - self.assertEqual(expected, actual) - - def test_slack_id_is_replaced(self): - slack_id = 'U12345' - slack_token = '<@' + slack_id + '>' - expected_member = Member(username="test_username") - - with patch.object(Member, "get_member_by_slack_id", return_value=expected_member) as test_method: - actual = parser.replace_slack_id_tokens_with_usernames(slack_token) - - self.assertEqual(expected_member.username, actual) - test_method.assert_called_once_with(slack_id) - - def test_slack_ids_replaced_in_long_message(self): - slack_id = 'U12345' - expected_member = Member(username="test_username") - - message = "Hello this a longer message with a <@U12345> slack id in it." - expected = "Hello this a longer message with a test_username slack id in it." - - with patch.object(Member, "get_member_by_slack_id", return_value=expected_member) as test_method: - actual = parser.replace_slack_id_tokens_with_usernames(message) - - self.assertEqual(expected, actual) - test_method.assert_called_once_with(slack_id) - - - if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_urban_event_handler.py b/tests/slack_channel_tests/test_urban_event_handler.py deleted file mode 100644 index f6ac637..0000000 --- a/tests/slack_channel_tests/test_urban_event_handler.py +++ /dev/null @@ -1,72 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import UrbanSearchCommand -from config import Config -from slack_channel import UrbanEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "ud a valid Urban command"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "ud -?"} -urban_response = "Doubtless some pile of filth" - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestUrbanEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = UrbanEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = UrbanEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(UrbanEventHandler, "_send_message_response") - @patch.object(UrbanSearchCommand, "execute", return_value = urban_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = UrbanEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with(valid_event["text"][4:]) - - @patch.object(UrbanSearchCommand, "execute", return_value=urban_response) - @patch.object(UrbanEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = UrbanEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with( - response_message=urban_response, - slack_event=valid_event, - as_reply=True) - - @patch.object(UrbanSearchCommand, "execute", return_value=None) - @patch.object(UrbanEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = UrbanEventHandler() - handler.handle(valid_event) - empty_results_message = "Left as an exercise for the reader." - response_method.assert_called_once_with( - response_message=empty_results_message, - slack_event=valid_event, - as_reply=True) - - @patch.object(UrbanSearchCommand, "get_purpose", return_value="Urbans") - @patch.object(UrbanEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = UrbanEventHandler() - handler.handle(help_event) - expected_help = "Urban Dictionary" + os.linesep + \ - "Urbans"+ os.linesep +\ - "Usage: `" + test_prefix + "ud " + "`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - - if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/slack_channel_tests/test_wikipedia_event_handler.py b/tests/slack_channel_tests/test_wikipedia_event_handler.py deleted file mode 100644 index 91dbea7..0000000 --- a/tests/slack_channel_tests/test_wikipedia_event_handler.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding=utf-8 -import unittest -from unittest.mock import patch - -import os - -from commands import WikipediaSearchCommand -from config import Config -from slack_channel import WikipediaEventHandler - -test_prefix = "_" -valid_event = {"text": test_prefix + "wik a valid Wikipedia command"} -invalid_event = {"text": "this is missing a valid command prefix"} -help_event = {"text": test_prefix + "wik -?"} -wikipedia_response = {"text": "response title", "link": "response_link"} - - -@patch.object(Config, "get_config_value") -@patch.object(Config, "get_prefix", return_value=test_prefix) -class TestWikipediaEventHandler(unittest.TestCase): - - def test_handles_correct_event(self, prefix_method, config_method): - handler = WikipediaEventHandler() - can_handle = handler.can_handle(valid_event) - self.assertTrue(can_handle) - - def test_does_not_handle_different_event(self, prefix_method, config_method): - handler = WikipediaEventHandler() - can_handle = handler.can_handle(invalid_event) - self.assertFalse(can_handle) - - @patch.object(WikipediaEventHandler, "_send_message_response") - @patch.object(WikipediaSearchCommand, "execute", return_value = wikipedia_response) - def test_command_execute_is_called(self, execute_method, response_method, config_method, prefix_method): - handler = WikipediaEventHandler() - handler.handle(valid_event) - execute_method.assert_called_once_with(valid_event["text"][5:]) - - @patch.object(WikipediaSearchCommand, "execute", return_value=wikipedia_response) - @patch.object(WikipediaEventHandler, "_send_message_response") - def test_command_result_is_correctly_built(self, response_method, execute_method, config_method, prefix_method): - handler = WikipediaEventHandler() - handler.handle(valid_event) - response_method.assert_called_once_with("response title response_link", valid_event) - - @patch.object(WikipediaSearchCommand, "execute", return_value=None) - @patch.object(WikipediaEventHandler, "_send_message_response") - def test_no_result_message_correctly_sent(self, response_method, execute_method, config_method, prefix_method): - handler = WikipediaEventHandler() - handler.handle(valid_event) - empty_results_message = "No results found for \"" + valid_event["text"][5:] + "\"" - response_method.assert_called_once_with(empty_results_message, valid_event) - - @patch.object(WikipediaSearchCommand, "get_purpose", return_value="Wikipedias") - @patch.object(WikipediaEventHandler, "_send_message_response") - def test_get_usage(self, response_method, purpose_method, config_method, prefix_method): - handler = WikipediaEventHandler() - handler.handle(help_event) - expected_help = "Wikipedia" + os.linesep + \ - "Wikipedias" + os.linesep + \ - "Usage: `" + test_prefix + "wik " + "`" - purpose_method.assert_called_once_with() - response_method.assert_called_once_with(expected_help, help_event) - - if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index a642b2d..0000000 --- a/tests/test_config.py +++ /dev/null @@ -1,15 +0,0 @@ -import unittest -from config import ConfigKeys, Config - - -class TestConfig(unittest.TestCase): - def test_all_defined_config_keys_configured(self): - config = Config() - config_values = [config.get_config_value(k) for k in ConfigKeys] - - for val in config_values: - self.assertIsNotNone(val) - - -if __name__ == '__main__': - unittest.main() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 9d5c2d8..0000000 --- a/tox.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -ignore = E226,E302,E41 -max-line-length = 88 -exclude = tests/*,venv/* -max-complexity = 10 \ No newline at end of file From 866221183500a18821795e33277f41047ba0b9f2 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 09:56:05 +0000 Subject: [PATCH 02/22] Move .NET project files from bottomly.net/ to repo root Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bottomly.net/.editorconfig => .editorconfig | 0 .gitignore | 585 +++++++++++++++--- {bottomly.net/.nuget => .nuget}/NuGet.config | 0 .vscode/launch.json | 15 + .vscode/settings.json | 18 + .../.aspire/settings.json | 0 .../AppHost.cs | 0 .../Bottomly.AppHost.csproj | 0 .../Properties/launchSettings.json | 0 .../apphost.run.json | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../Bottomly.ServiceDefaults.csproj | 0 .../Extensions.cs | 0 .../Bottomly.Tests.csproj | 0 .../Commands/AddKarmaCommandTests.cs | 0 .../Commands/GiphyCommandTests.cs | 0 .../Commands/GoogleImageSearchCommandTests.cs | 0 .../Commands/GoogleSearchCommandTests.cs | 0 .../Commands/RegSearchCommandTests.cs | 0 .../Commands/UrbanSearchCommandTests.cs | 0 .../Commands/WikipediaSearchCommandTests.cs | 0 .../Helpers/TestHelpers.cs | 0 .../LlmBot/LlmBotTests.cs | 0 .../LlmBot/LlmClientTests.cs | 0 .../Repositories/KarmaRepositoryTests.cs | 0 .../Repositories/MemberRepositoryTests.cs | 0 .../GetCurrentKarmaReasonsHandlerTests.cs | 0 .../GetCurrentNetKarmaHandlerTests.cs | 0 .../GetLeaderBoardHandlerTests.cs | 0 .../GetLoserBoardHandlerTests.cs | 0 .../Slack/EventHandlers/GiphyHandlerTests.cs | 0 .../Slack/EventHandlers/GoogleHandlerTests.cs | 0 .../EventHandlers/GoogleImageHandlerTests.cs | 0 .../Slack/EventHandlers/HelpHandlerTests.cs | 0 .../DecrementMessageKarmaEventHandlerTests.cs | 0 .../IncrementMessageKarmaEventHandlerTests.cs | 0 .../KarmaHandlerCommandParsingTests.cs | 0 .../Slack/EventHandlers/RegHandlerTests.cs | 0 .../EventHandlers/ReleaseHandlerTests.cs | 0 .../Slack/EventHandlers/UrbanHandlerTests.cs | 0 .../EventHandlers/WikipediaHandlerTests.cs | 0 .../Slack/MemberlistPopulatorTests.cs | 0 .../MemberJoinedEventHandlerTests.cs | 0 .../ConversationMessageHandlerTests.cs | 0 .../MessageContextExtensionsTests.cs | 0 .../ResponseMessageFactoryTests.cs | 0 .../AddKarmaReactionHandlerTests.cs | 0 .../Slack/SlackMessageBrokerTests.cs | 0 .../Slack/SlackParserTests.cs | 0 .../Slack/SlackWorkerTests.cs | 0 .../Bottomly => Bottomly}/Bottomly.csproj | 0 .../Commands/AddKarmaCommand.cs | 0 .../Commands/GiphyCommand.cs | 0 .../Commands/GoogleImageSearchCommand.cs | 0 .../Commands/GoogleSearchCommand.cs | 0 .../Commands/ICommand.cs | 0 .../Commands/RegSearchCommand.cs | 0 .../Commands/ReleaseCommand.cs | 0 .../Commands/UrbanSearchCommand.cs | 0 .../Commands/WikipediaSearchCommand.cs | 0 .../Configuration/BottomlyOptions.cs | 0 .../LlmBot/BottomlyInputMessage.cs | 0 .../LlmBot/BottomlyUserNote.cs | 0 .../LlmBot/FullPromptContext.cs | 0 .../Bottomly => Bottomly}/LlmBot/LlmClient.cs | 0 .../LlmBot/MessageHistoryContext.cs | 0 .../Bottomly => Bottomly}/Models/Gender.cs | 0 .../Bottomly => Bottomly}/Models/Karma.cs | 0 .../Bottomly => Bottomly}/Models/KarmaType.cs | 0 .../Bottomly => Bottomly}/Models/Member.cs | 0 .../Bottomly => Bottomly}/Models/SassLevel.cs | 0 .../Bottomly => Bottomly}/Program.cs | 0 .../Repositories/IKarmaRepository.cs | 0 .../Repositories/IMemberRepository.cs | 0 .../Repositories/KarmaRepository.cs | 0 .../Repositories/MemberRepository.cs | 0 .../Seed/MemberSeedDataDto.cs | 0 .../Seed/MemberSeedDataImporter.cs | 0 .../Slack/ISlackMessageBroker.cs | 0 .../Slack/MemberlistPopulator.cs | 0 .../MemberJoinedEventHandler.cs | 0 .../AbstractMessageEventHandler.cs | 0 .../ConversationMessageHandler.cs | 0 .../MessageContextExtensions.cs | 0 .../ResponseMessageFactory.cs | 0 .../GetCurrentKarmaReasonsHandler.cs | 0 .../GetCurrentNetKarmaHandler.cs | 0 .../GetLeaderBoardHandler.cs | 0 .../GetLoserBoardHandler.cs | 0 .../MessageEventHandlers/GiphyHandler.cs | 0 .../MessageEventHandlers/GoogleHandler.cs | 0 .../GoogleImageHandler.cs | 0 .../Slack/MessageEventHandlers/HelpHandler.cs | 0 .../IMessageEventHandler.cs | 0 .../AbstractMessageKarmaEventHandler.cs | 0 .../DecrementMessageKarmaEventHandler.cs | 0 .../IncrementMessageKarmaEventHandler.cs | 0 .../Slack/MessageEventHandlers/RegHandler.cs | 0 .../MessageEventHandlers/ReleaseHandler.cs | 0 .../Slack/MessageEventHandlers/TestHandler.cs | 0 .../MessageEventHandlers/UrbanHandler.cs | 0 .../MessageEventHandlers/WikipediaHandler.cs | 0 .../AddKarmaReactionHandler.cs | 0 .../ReactionHandlers/IReactionHandler.cs | 0 .../ReactionHandlers/KarmaReactionMap.cs | 0 .../Slack/SlackEventDispatchers.cs | 0 .../Slack/SlackMessageBroker.cs | 0 .../Slack/SlackParser.cs | 0 .../Slack/SlackWorker.cs | 0 .../Bottomly => Bottomly}/appsettings.json | 0 .../bottomly.net.slnx => bottomly.net.slnx | 0 bottomly.net/.gitignore | 518 ---------------- 113 files changed, 526 insertions(+), 610 deletions(-) rename bottomly.net/.editorconfig => .editorconfig (100%) rename {bottomly.net/.nuget => .nuget}/NuGet.config (100%) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/.aspire/settings.json (100%) rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/AppHost.cs (100%) rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/Bottomly.AppHost.csproj (100%) rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/Properties/launchSettings.json (100%) rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/apphost.run.json (100%) rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/appsettings.Development.json (100%) rename {bottomly.net/Bottomly.AppHost => Bottomly.AppHost}/appsettings.json (100%) rename {bottomly.net/Bottomly.ServiceDefaults => Bottomly.ServiceDefaults}/Bottomly.ServiceDefaults.csproj (100%) rename {bottomly.net/Bottomly.ServiceDefaults => Bottomly.ServiceDefaults}/Extensions.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Bottomly.Tests.csproj (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/AddKarmaCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/GiphyCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/GoogleImageSearchCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/GoogleSearchCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/RegSearchCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/UrbanSearchCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Commands/WikipediaSearchCommandTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Helpers/TestHelpers.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/LlmBot/LlmBotTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/LlmBot/LlmClientTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Repositories/KarmaRepositoryTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Repositories/MemberRepositoryTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GetCurrentKarmaReasonsHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GetLeaderBoardHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GetLoserBoardHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GiphyHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GoogleHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/GoogleImageHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/HelpHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/KarmaEventHandlers/KarmaHandlerCommandParsingTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/RegHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/ReleaseHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/UrbanHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/EventHandlers/WikipediaHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/MemberlistPopulatorTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/MembershipEventHandlers/MemberJoinedEventHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensionsTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactoryTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/ReactionHandlers/AddKarmaReactionHandlerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/SlackMessageBrokerTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/SlackParserTests.cs (100%) rename {bottomly.net/Bottomly.Tests => Bottomly.Tests}/Slack/SlackWorkerTests.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Bottomly.csproj (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/AddKarmaCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/GiphyCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/GoogleImageSearchCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/GoogleSearchCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/ICommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/RegSearchCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/ReleaseCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/UrbanSearchCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Commands/WikipediaSearchCommand.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Configuration/BottomlyOptions.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/LlmBot/BottomlyInputMessage.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/LlmBot/BottomlyUserNote.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/LlmBot/FullPromptContext.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/LlmBot/LlmClient.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/LlmBot/MessageHistoryContext.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Models/Gender.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Models/Karma.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Models/KarmaType.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Models/Member.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Models/SassLevel.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Program.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Repositories/IKarmaRepository.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Repositories/IMemberRepository.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Repositories/KarmaRepository.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Repositories/MemberRepository.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Seed/MemberSeedDataDto.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Seed/MemberSeedDataImporter.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/ISlackMessageBroker.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MemberlistPopulator.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MembershipEventHandlers/MemberJoinedEventHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/AbstractMessageEventHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensions.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactory.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GetCurrentKarmaReasonsHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GetLeaderBoardHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GetLoserBoardHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GiphyHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GoogleHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/GoogleImageHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/HelpHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/IMessageEventHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/KarmaEventHandlers/AbstractMessageKarmaEventHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/RegHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/ReleaseHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/TestHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/UrbanHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/MessageEventHandlers/WikipediaHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/ReactionHandlers/AddKarmaReactionHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/ReactionHandlers/IReactionHandler.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/ReactionHandlers/KarmaReactionMap.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/SlackEventDispatchers.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/SlackMessageBroker.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/SlackParser.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/Slack/SlackWorker.cs (100%) rename {bottomly.net/Bottomly => Bottomly}/appsettings.json (100%) rename bottomly.net/bottomly.net.slnx => bottomly.net.slnx (100%) delete mode 100644 bottomly.net/.gitignore diff --git a/bottomly.net/.editorconfig b/.editorconfig similarity index 100% rename from bottomly.net/.editorconfig rename to .editorconfig diff --git a/.gitignore b/.gitignore index 97402cf..b1918b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,117 +1,518 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# .NET and Visual Studio .gitignore -# C extensions -*.so +# Planning documents +.plans/ -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ +# Confidential member seed data +MemberSeedData/ + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt -# Translations -*.mo -*.pot +# StyleCop +StyleCopReport.xml -# Django stuff: +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj *.log -local_settings.py +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc -# Flask stuff: -instance/ -.webassets-cache +# Chutzpah Test files +_Chutzpah* -# Scrapy stuff: -.scrapy +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb -# Sphinx documentation -docs/_build/ +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap -# PyBuilder -target/ +# Visual Studio Trace Files +*.e2e -# Jupyter Notebook -.ipynb_checkpoints +# TFS 2012 Local Workspace +$tf/ -# pyenv -.python-version +# Guidance Automation Toolkit +*.gpState -# celery beat schedule file -celerybeat-schedule +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user -# SageMath parsed files -*.sage.py +# TeamCity is a build add-in +_TeamCity* -# dotenv -.env +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ -# virtualenv -.venv -venv/ -ENV/ +# CodeRush personal settings +.cr/personal -# Spyder project settings -.spyderproject -.spyproject +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb -# Rope project settings -.ropeproject +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ -# mkdocs documentation -/site +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ -# mypy -.mypy_cache/ +# Fody - auto-generated XML schema +FodyWeavers.xsd -# pycharm +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Mono auto generated files +mono_crash.* + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac desktop service store files +.DS_Store + +# JetBrains IDEs .idea/ +*.iml +*.ipr +*.iws + +# Temporary files +*.tmp +*.temp +*.bak +*.swp +*.swo -# VS Code -.vscode/ +# Log files +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Rollup.js default build output +dist/ +# Uncomment the following line if you want to include yarn.lock +# yarn.lock -# development environment configs -devenv.sh -devenv.bat -deploy-to-test.ps1 -deploy-to-live.ps1 +# Stores VSCode versions used for testing VSCode extensions +.vscode-test -# logs -bottomly.log +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/bottomly.net/.nuget/NuGet.config b/.nuget/NuGet.config similarity index 100% rename from bottomly.net/.nuget/NuGet.config rename to .nuget/NuGet.config diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6b76b4f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4ac3b43 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": false, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./tests", + "-p", + "test_*.py" + ], + "cSpell.words": [ + "Bottomly", + "sass" + ] +} \ No newline at end of file diff --git a/bottomly.net/Bottomly.AppHost/.aspire/settings.json b/Bottomly.AppHost/.aspire/settings.json similarity index 100% rename from bottomly.net/Bottomly.AppHost/.aspire/settings.json rename to Bottomly.AppHost/.aspire/settings.json diff --git a/bottomly.net/Bottomly.AppHost/AppHost.cs b/Bottomly.AppHost/AppHost.cs similarity index 100% rename from bottomly.net/Bottomly.AppHost/AppHost.cs rename to Bottomly.AppHost/AppHost.cs diff --git a/bottomly.net/Bottomly.AppHost/Bottomly.AppHost.csproj b/Bottomly.AppHost/Bottomly.AppHost.csproj similarity index 100% rename from bottomly.net/Bottomly.AppHost/Bottomly.AppHost.csproj rename to Bottomly.AppHost/Bottomly.AppHost.csproj diff --git a/bottomly.net/Bottomly.AppHost/Properties/launchSettings.json b/Bottomly.AppHost/Properties/launchSettings.json similarity index 100% rename from bottomly.net/Bottomly.AppHost/Properties/launchSettings.json rename to Bottomly.AppHost/Properties/launchSettings.json diff --git a/bottomly.net/Bottomly.AppHost/apphost.run.json b/Bottomly.AppHost/apphost.run.json similarity index 100% rename from bottomly.net/Bottomly.AppHost/apphost.run.json rename to Bottomly.AppHost/apphost.run.json diff --git a/bottomly.net/Bottomly.AppHost/appsettings.Development.json b/Bottomly.AppHost/appsettings.Development.json similarity index 100% rename from bottomly.net/Bottomly.AppHost/appsettings.Development.json rename to Bottomly.AppHost/appsettings.Development.json diff --git a/bottomly.net/Bottomly.AppHost/appsettings.json b/Bottomly.AppHost/appsettings.json similarity index 100% rename from bottomly.net/Bottomly.AppHost/appsettings.json rename to Bottomly.AppHost/appsettings.json diff --git a/bottomly.net/Bottomly.ServiceDefaults/Bottomly.ServiceDefaults.csproj b/Bottomly.ServiceDefaults/Bottomly.ServiceDefaults.csproj similarity index 100% rename from bottomly.net/Bottomly.ServiceDefaults/Bottomly.ServiceDefaults.csproj rename to Bottomly.ServiceDefaults/Bottomly.ServiceDefaults.csproj diff --git a/bottomly.net/Bottomly.ServiceDefaults/Extensions.cs b/Bottomly.ServiceDefaults/Extensions.cs similarity index 100% rename from bottomly.net/Bottomly.ServiceDefaults/Extensions.cs rename to Bottomly.ServiceDefaults/Extensions.cs diff --git a/bottomly.net/Bottomly.Tests/Bottomly.Tests.csproj b/Bottomly.Tests/Bottomly.Tests.csproj similarity index 100% rename from bottomly.net/Bottomly.Tests/Bottomly.Tests.csproj rename to Bottomly.Tests/Bottomly.Tests.csproj diff --git a/bottomly.net/Bottomly.Tests/Commands/AddKarmaCommandTests.cs b/Bottomly.Tests/Commands/AddKarmaCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/AddKarmaCommandTests.cs rename to Bottomly.Tests/Commands/AddKarmaCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Commands/GiphyCommandTests.cs b/Bottomly.Tests/Commands/GiphyCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/GiphyCommandTests.cs rename to Bottomly.Tests/Commands/GiphyCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Commands/GoogleImageSearchCommandTests.cs b/Bottomly.Tests/Commands/GoogleImageSearchCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/GoogleImageSearchCommandTests.cs rename to Bottomly.Tests/Commands/GoogleImageSearchCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Commands/GoogleSearchCommandTests.cs b/Bottomly.Tests/Commands/GoogleSearchCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/GoogleSearchCommandTests.cs rename to Bottomly.Tests/Commands/GoogleSearchCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Commands/RegSearchCommandTests.cs b/Bottomly.Tests/Commands/RegSearchCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/RegSearchCommandTests.cs rename to Bottomly.Tests/Commands/RegSearchCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Commands/UrbanSearchCommandTests.cs b/Bottomly.Tests/Commands/UrbanSearchCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/UrbanSearchCommandTests.cs rename to Bottomly.Tests/Commands/UrbanSearchCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Commands/WikipediaSearchCommandTests.cs b/Bottomly.Tests/Commands/WikipediaSearchCommandTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Commands/WikipediaSearchCommandTests.cs rename to Bottomly.Tests/Commands/WikipediaSearchCommandTests.cs diff --git a/bottomly.net/Bottomly.Tests/Helpers/TestHelpers.cs b/Bottomly.Tests/Helpers/TestHelpers.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Helpers/TestHelpers.cs rename to Bottomly.Tests/Helpers/TestHelpers.cs diff --git a/bottomly.net/Bottomly.Tests/LlmBot/LlmBotTests.cs b/Bottomly.Tests/LlmBot/LlmBotTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/LlmBot/LlmBotTests.cs rename to Bottomly.Tests/LlmBot/LlmBotTests.cs diff --git a/bottomly.net/Bottomly.Tests/LlmBot/LlmClientTests.cs b/Bottomly.Tests/LlmBot/LlmClientTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/LlmBot/LlmClientTests.cs rename to Bottomly.Tests/LlmBot/LlmClientTests.cs diff --git a/bottomly.net/Bottomly.Tests/Repositories/KarmaRepositoryTests.cs b/Bottomly.Tests/Repositories/KarmaRepositoryTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Repositories/KarmaRepositoryTests.cs rename to Bottomly.Tests/Repositories/KarmaRepositoryTests.cs diff --git a/bottomly.net/Bottomly.Tests/Repositories/MemberRepositoryTests.cs b/Bottomly.Tests/Repositories/MemberRepositoryTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Repositories/MemberRepositoryTests.cs rename to Bottomly.Tests/Repositories/MemberRepositoryTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetCurrentKarmaReasonsHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetCurrentKarmaReasonsHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetCurrentKarmaReasonsHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GetCurrentKarmaReasonsHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetLeaderBoardHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetLeaderBoardHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetLeaderBoardHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GetLeaderBoardHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetLoserBoardHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetLoserBoardHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetLoserBoardHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GetLoserBoardHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GiphyHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GiphyHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GiphyHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GiphyHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GoogleHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GoogleHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GoogleHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GoogleHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/GoogleImageHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GoogleImageHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GoogleImageHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GoogleImageHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/HelpHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/HelpHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/HelpHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/HelpHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/KarmaHandlerCommandParsingTests.cs b/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/KarmaHandlerCommandParsingTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/KarmaHandlerCommandParsingTests.cs rename to Bottomly.Tests/Slack/EventHandlers/KarmaEventHandlers/KarmaHandlerCommandParsingTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/RegHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/RegHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/RegHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/RegHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/ReleaseHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/ReleaseHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/ReleaseHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/ReleaseHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/UrbanHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/UrbanHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/UrbanHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/UrbanHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/EventHandlers/WikipediaHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/WikipediaHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/WikipediaHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/WikipediaHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/MemberlistPopulatorTests.cs b/Bottomly.Tests/Slack/MemberlistPopulatorTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/MemberlistPopulatorTests.cs rename to Bottomly.Tests/Slack/MemberlistPopulatorTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/MembershipEventHandlers/MemberJoinedEventHandlerTests.cs b/Bottomly.Tests/Slack/MembershipEventHandlers/MemberJoinedEventHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/MembershipEventHandlers/MemberJoinedEventHandlerTests.cs rename to Bottomly.Tests/Slack/MembershipEventHandlers/MemberJoinedEventHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandlerTests.cs b/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandlerTests.cs rename to Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensionsTests.cs b/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensionsTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensionsTests.cs rename to Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensionsTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactoryTests.cs b/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactoryTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactoryTests.cs rename to Bottomly.Tests/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactoryTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/ReactionHandlers/AddKarmaReactionHandlerTests.cs b/Bottomly.Tests/Slack/ReactionHandlers/AddKarmaReactionHandlerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/ReactionHandlers/AddKarmaReactionHandlerTests.cs rename to Bottomly.Tests/Slack/ReactionHandlers/AddKarmaReactionHandlerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/SlackMessageBrokerTests.cs b/Bottomly.Tests/Slack/SlackMessageBrokerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/SlackMessageBrokerTests.cs rename to Bottomly.Tests/Slack/SlackMessageBrokerTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/SlackParserTests.cs b/Bottomly.Tests/Slack/SlackParserTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/SlackParserTests.cs rename to Bottomly.Tests/Slack/SlackParserTests.cs diff --git a/bottomly.net/Bottomly.Tests/Slack/SlackWorkerTests.cs b/Bottomly.Tests/Slack/SlackWorkerTests.cs similarity index 100% rename from bottomly.net/Bottomly.Tests/Slack/SlackWorkerTests.cs rename to Bottomly.Tests/Slack/SlackWorkerTests.cs diff --git a/bottomly.net/Bottomly/Bottomly.csproj b/Bottomly/Bottomly.csproj similarity index 100% rename from bottomly.net/Bottomly/Bottomly.csproj rename to Bottomly/Bottomly.csproj diff --git a/bottomly.net/Bottomly/Commands/AddKarmaCommand.cs b/Bottomly/Commands/AddKarmaCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/AddKarmaCommand.cs rename to Bottomly/Commands/AddKarmaCommand.cs diff --git a/bottomly.net/Bottomly/Commands/GiphyCommand.cs b/Bottomly/Commands/GiphyCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/GiphyCommand.cs rename to Bottomly/Commands/GiphyCommand.cs diff --git a/bottomly.net/Bottomly/Commands/GoogleImageSearchCommand.cs b/Bottomly/Commands/GoogleImageSearchCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/GoogleImageSearchCommand.cs rename to Bottomly/Commands/GoogleImageSearchCommand.cs diff --git a/bottomly.net/Bottomly/Commands/GoogleSearchCommand.cs b/Bottomly/Commands/GoogleSearchCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/GoogleSearchCommand.cs rename to Bottomly/Commands/GoogleSearchCommand.cs diff --git a/bottomly.net/Bottomly/Commands/ICommand.cs b/Bottomly/Commands/ICommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/ICommand.cs rename to Bottomly/Commands/ICommand.cs diff --git a/bottomly.net/Bottomly/Commands/RegSearchCommand.cs b/Bottomly/Commands/RegSearchCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/RegSearchCommand.cs rename to Bottomly/Commands/RegSearchCommand.cs diff --git a/bottomly.net/Bottomly/Commands/ReleaseCommand.cs b/Bottomly/Commands/ReleaseCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/ReleaseCommand.cs rename to Bottomly/Commands/ReleaseCommand.cs diff --git a/bottomly.net/Bottomly/Commands/UrbanSearchCommand.cs b/Bottomly/Commands/UrbanSearchCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/UrbanSearchCommand.cs rename to Bottomly/Commands/UrbanSearchCommand.cs diff --git a/bottomly.net/Bottomly/Commands/WikipediaSearchCommand.cs b/Bottomly/Commands/WikipediaSearchCommand.cs similarity index 100% rename from bottomly.net/Bottomly/Commands/WikipediaSearchCommand.cs rename to Bottomly/Commands/WikipediaSearchCommand.cs diff --git a/bottomly.net/Bottomly/Configuration/BottomlyOptions.cs b/Bottomly/Configuration/BottomlyOptions.cs similarity index 100% rename from bottomly.net/Bottomly/Configuration/BottomlyOptions.cs rename to Bottomly/Configuration/BottomlyOptions.cs diff --git a/bottomly.net/Bottomly/LlmBot/BottomlyInputMessage.cs b/Bottomly/LlmBot/BottomlyInputMessage.cs similarity index 100% rename from bottomly.net/Bottomly/LlmBot/BottomlyInputMessage.cs rename to Bottomly/LlmBot/BottomlyInputMessage.cs diff --git a/bottomly.net/Bottomly/LlmBot/BottomlyUserNote.cs b/Bottomly/LlmBot/BottomlyUserNote.cs similarity index 100% rename from bottomly.net/Bottomly/LlmBot/BottomlyUserNote.cs rename to Bottomly/LlmBot/BottomlyUserNote.cs diff --git a/bottomly.net/Bottomly/LlmBot/FullPromptContext.cs b/Bottomly/LlmBot/FullPromptContext.cs similarity index 100% rename from bottomly.net/Bottomly/LlmBot/FullPromptContext.cs rename to Bottomly/LlmBot/FullPromptContext.cs diff --git a/bottomly.net/Bottomly/LlmBot/LlmClient.cs b/Bottomly/LlmBot/LlmClient.cs similarity index 100% rename from bottomly.net/Bottomly/LlmBot/LlmClient.cs rename to Bottomly/LlmBot/LlmClient.cs diff --git a/bottomly.net/Bottomly/LlmBot/MessageHistoryContext.cs b/Bottomly/LlmBot/MessageHistoryContext.cs similarity index 100% rename from bottomly.net/Bottomly/LlmBot/MessageHistoryContext.cs rename to Bottomly/LlmBot/MessageHistoryContext.cs diff --git a/bottomly.net/Bottomly/Models/Gender.cs b/Bottomly/Models/Gender.cs similarity index 100% rename from bottomly.net/Bottomly/Models/Gender.cs rename to Bottomly/Models/Gender.cs diff --git a/bottomly.net/Bottomly/Models/Karma.cs b/Bottomly/Models/Karma.cs similarity index 100% rename from bottomly.net/Bottomly/Models/Karma.cs rename to Bottomly/Models/Karma.cs diff --git a/bottomly.net/Bottomly/Models/KarmaType.cs b/Bottomly/Models/KarmaType.cs similarity index 100% rename from bottomly.net/Bottomly/Models/KarmaType.cs rename to Bottomly/Models/KarmaType.cs diff --git a/bottomly.net/Bottomly/Models/Member.cs b/Bottomly/Models/Member.cs similarity index 100% rename from bottomly.net/Bottomly/Models/Member.cs rename to Bottomly/Models/Member.cs diff --git a/bottomly.net/Bottomly/Models/SassLevel.cs b/Bottomly/Models/SassLevel.cs similarity index 100% rename from bottomly.net/Bottomly/Models/SassLevel.cs rename to Bottomly/Models/SassLevel.cs diff --git a/bottomly.net/Bottomly/Program.cs b/Bottomly/Program.cs similarity index 100% rename from bottomly.net/Bottomly/Program.cs rename to Bottomly/Program.cs diff --git a/bottomly.net/Bottomly/Repositories/IKarmaRepository.cs b/Bottomly/Repositories/IKarmaRepository.cs similarity index 100% rename from bottomly.net/Bottomly/Repositories/IKarmaRepository.cs rename to Bottomly/Repositories/IKarmaRepository.cs diff --git a/bottomly.net/Bottomly/Repositories/IMemberRepository.cs b/Bottomly/Repositories/IMemberRepository.cs similarity index 100% rename from bottomly.net/Bottomly/Repositories/IMemberRepository.cs rename to Bottomly/Repositories/IMemberRepository.cs diff --git a/bottomly.net/Bottomly/Repositories/KarmaRepository.cs b/Bottomly/Repositories/KarmaRepository.cs similarity index 100% rename from bottomly.net/Bottomly/Repositories/KarmaRepository.cs rename to Bottomly/Repositories/KarmaRepository.cs diff --git a/bottomly.net/Bottomly/Repositories/MemberRepository.cs b/Bottomly/Repositories/MemberRepository.cs similarity index 100% rename from bottomly.net/Bottomly/Repositories/MemberRepository.cs rename to Bottomly/Repositories/MemberRepository.cs diff --git a/bottomly.net/Bottomly/Seed/MemberSeedDataDto.cs b/Bottomly/Seed/MemberSeedDataDto.cs similarity index 100% rename from bottomly.net/Bottomly/Seed/MemberSeedDataDto.cs rename to Bottomly/Seed/MemberSeedDataDto.cs diff --git a/bottomly.net/Bottomly/Seed/MemberSeedDataImporter.cs b/Bottomly/Seed/MemberSeedDataImporter.cs similarity index 100% rename from bottomly.net/Bottomly/Seed/MemberSeedDataImporter.cs rename to Bottomly/Seed/MemberSeedDataImporter.cs diff --git a/bottomly.net/Bottomly/Slack/ISlackMessageBroker.cs b/Bottomly/Slack/ISlackMessageBroker.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/ISlackMessageBroker.cs rename to Bottomly/Slack/ISlackMessageBroker.cs diff --git a/bottomly.net/Bottomly/Slack/MemberlistPopulator.cs b/Bottomly/Slack/MemberlistPopulator.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MemberlistPopulator.cs rename to Bottomly/Slack/MemberlistPopulator.cs diff --git a/bottomly.net/Bottomly/Slack/MembershipEventHandlers/MemberJoinedEventHandler.cs b/Bottomly/Slack/MembershipEventHandlers/MemberJoinedEventHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MembershipEventHandlers/MemberJoinedEventHandler.cs rename to Bottomly/Slack/MembershipEventHandlers/MemberJoinedEventHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/AbstractMessageEventHandler.cs b/Bottomly/Slack/MessageEventHandlers/AbstractMessageEventHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/AbstractMessageEventHandler.cs rename to Bottomly/Slack/MessageEventHandlers/AbstractMessageEventHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandler.cs b/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandler.cs rename to Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ConversationMessageHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensions.cs b/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensions.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensions.cs rename to Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/MessageContextExtensions.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactory.cs b/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactory.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactory.cs rename to Bottomly/Slack/MessageEventHandlers/ConversationMessageHandling/ResponseMessageFactory.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GetCurrentKarmaReasonsHandler.cs b/Bottomly/Slack/MessageEventHandlers/GetCurrentKarmaReasonsHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GetCurrentKarmaReasonsHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GetCurrentKarmaReasonsHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs b/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GetLeaderBoardHandler.cs b/Bottomly/Slack/MessageEventHandlers/GetLeaderBoardHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GetLeaderBoardHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GetLeaderBoardHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GetLoserBoardHandler.cs b/Bottomly/Slack/MessageEventHandlers/GetLoserBoardHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GetLoserBoardHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GetLoserBoardHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GiphyHandler.cs b/Bottomly/Slack/MessageEventHandlers/GiphyHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GiphyHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GiphyHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GoogleHandler.cs b/Bottomly/Slack/MessageEventHandlers/GoogleHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GoogleHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GoogleHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/GoogleImageHandler.cs b/Bottomly/Slack/MessageEventHandlers/GoogleImageHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GoogleImageHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GoogleImageHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/HelpHandler.cs b/Bottomly/Slack/MessageEventHandlers/HelpHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/HelpHandler.cs rename to Bottomly/Slack/MessageEventHandlers/HelpHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/IMessageEventHandler.cs b/Bottomly/Slack/MessageEventHandlers/IMessageEventHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/IMessageEventHandler.cs rename to Bottomly/Slack/MessageEventHandlers/IMessageEventHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/AbstractMessageKarmaEventHandler.cs b/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/AbstractMessageKarmaEventHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/AbstractMessageKarmaEventHandler.cs rename to Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/AbstractMessageKarmaEventHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandler.cs b/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandler.cs rename to Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/DecrementMessageKarmaEventHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandler.cs b/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandler.cs rename to Bottomly/Slack/MessageEventHandlers/KarmaEventHandlers/IncrementMessageKarmaEventHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/RegHandler.cs b/Bottomly/Slack/MessageEventHandlers/RegHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/RegHandler.cs rename to Bottomly/Slack/MessageEventHandlers/RegHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/ReleaseHandler.cs b/Bottomly/Slack/MessageEventHandlers/ReleaseHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/ReleaseHandler.cs rename to Bottomly/Slack/MessageEventHandlers/ReleaseHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/TestHandler.cs b/Bottomly/Slack/MessageEventHandlers/TestHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/TestHandler.cs rename to Bottomly/Slack/MessageEventHandlers/TestHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/UrbanHandler.cs b/Bottomly/Slack/MessageEventHandlers/UrbanHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/UrbanHandler.cs rename to Bottomly/Slack/MessageEventHandlers/UrbanHandler.cs diff --git a/bottomly.net/Bottomly/Slack/MessageEventHandlers/WikipediaHandler.cs b/Bottomly/Slack/MessageEventHandlers/WikipediaHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/WikipediaHandler.cs rename to Bottomly/Slack/MessageEventHandlers/WikipediaHandler.cs diff --git a/bottomly.net/Bottomly/Slack/ReactionHandlers/AddKarmaReactionHandler.cs b/Bottomly/Slack/ReactionHandlers/AddKarmaReactionHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/ReactionHandlers/AddKarmaReactionHandler.cs rename to Bottomly/Slack/ReactionHandlers/AddKarmaReactionHandler.cs diff --git a/bottomly.net/Bottomly/Slack/ReactionHandlers/IReactionHandler.cs b/Bottomly/Slack/ReactionHandlers/IReactionHandler.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/ReactionHandlers/IReactionHandler.cs rename to Bottomly/Slack/ReactionHandlers/IReactionHandler.cs diff --git a/bottomly.net/Bottomly/Slack/ReactionHandlers/KarmaReactionMap.cs b/Bottomly/Slack/ReactionHandlers/KarmaReactionMap.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/ReactionHandlers/KarmaReactionMap.cs rename to Bottomly/Slack/ReactionHandlers/KarmaReactionMap.cs diff --git a/bottomly.net/Bottomly/Slack/SlackEventDispatchers.cs b/Bottomly/Slack/SlackEventDispatchers.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/SlackEventDispatchers.cs rename to Bottomly/Slack/SlackEventDispatchers.cs diff --git a/bottomly.net/Bottomly/Slack/SlackMessageBroker.cs b/Bottomly/Slack/SlackMessageBroker.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/SlackMessageBroker.cs rename to Bottomly/Slack/SlackMessageBroker.cs diff --git a/bottomly.net/Bottomly/Slack/SlackParser.cs b/Bottomly/Slack/SlackParser.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/SlackParser.cs rename to Bottomly/Slack/SlackParser.cs diff --git a/bottomly.net/Bottomly/Slack/SlackWorker.cs b/Bottomly/Slack/SlackWorker.cs similarity index 100% rename from bottomly.net/Bottomly/Slack/SlackWorker.cs rename to Bottomly/Slack/SlackWorker.cs diff --git a/bottomly.net/Bottomly/appsettings.json b/Bottomly/appsettings.json similarity index 100% rename from bottomly.net/Bottomly/appsettings.json rename to Bottomly/appsettings.json diff --git a/bottomly.net/bottomly.net.slnx b/bottomly.net.slnx similarity index 100% rename from bottomly.net/bottomly.net.slnx rename to bottomly.net.slnx diff --git a/bottomly.net/.gitignore b/bottomly.net/.gitignore deleted file mode 100644 index d79eb76..0000000 --- a/bottomly.net/.gitignore +++ /dev/null @@ -1,518 +0,0 @@ -# .NET and Visual Studio .gitignore - -# Planning documents -.plans/ - -# Confidential member seed data -MemberSeedData/ - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml -.idea/ - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# Mono auto generated files -mono_crash.* - -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac desktop service store files -.DS_Store - -# JetBrains IDEs -.idea/ -*.iml -*.ipr -*.iws - -# Temporary files -*.tmp -*.temp -*.bak -*.swp -*.swo - -# Log files -*.log - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# nyc test coverage -.nyc_output - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test -.env.production - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -public - -# Storybook build outputs -.out -.storybook-out - -# Rollup.js default build output -dist/ - -# Uncomment the following line if you want to include yarn.lock -# yarn.lock - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* \ No newline at end of file From 660001ba35ce23ead68735c9be56243c1c3eaa95 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 09:58:51 +0000 Subject: [PATCH 03/22] Update README and dotnet.yml paths after moving files to repo root Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet.yml | 6 +++--- README.md | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3188b43..73a9801 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -22,10 +22,10 @@ jobs: run: dotnet workload install aspire - name: Restore dependencies - run: dotnet restore bottomly.net/bottomly.net.slnx + run: dotnet restore bottomly.net.slnx - name: Build - run: dotnet build bottomly.net/bottomly.net.slnx --no-restore --configuration Release + run: dotnet build bottomly.net.slnx --no-restore --configuration Release - name: Test - run: dotnet test bottomly.net/bottomly.net.slnx --no-build --configuration Release --verbosity normal + run: dotnet test bottomly.net.slnx --no-build --configuration Release --verbosity normal diff --git a/README.md b/README.md index ff3a4be..fa05186 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ A .NET Slack bot The project uses [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) for local orchestration. Running the `Bottomly.AppHost` project will spin up MongoDB, Ollama (LLM), and the bot itself: ```bash -cd bottomly.net dotnet run --project Bottomly.AppHost ``` @@ -20,7 +19,6 @@ dotnet run --project Bottomly.AppHost Run the `Bottomly` project directly, ensuring MongoDB and Ollama are available and all required environment variables are set: ```bash -cd bottomly.net dotnet run --project Bottomly ``` From 37e4d1b866419c1d45463801fccb32b4cbf66231 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:03:21 +0000 Subject: [PATCH 04/22] Add Dockerfile for Bottomly .NET worker service Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Dockerfile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a2b3fef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0 AS build +WORKDIR /src + +RUN dotnet workload install aspire + +COPY bottomly.net.slnx ./ +COPY Bottomly/Bottomly.csproj Bottomly/ +COPY Bottomly.ServiceDefaults/Bottomly.ServiceDefaults.csproj Bottomly.ServiceDefaults/ + +RUN dotnet restore Bottomly/Bottomly.csproj + +COPY Bottomly/ Bottomly/ +COPY Bottomly.ServiceDefaults/ Bottomly.ServiceDefaults/ + +RUN dotnet publish Bottomly/Bottomly.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/nightly/runtime:10.0 +WORKDIR /app +COPY --from=build /app/publish . + +ENTRYPOINT ["dotnet", "Bottomly.dll"] From 42d33e3795a05e85b033cd185e3eabc97a26b2b1 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:04:56 +0000 Subject: [PATCH 05/22] Switch Dockerfile to .NET 10 LTS images Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a2b3fef..6b71675 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src RUN dotnet workload install aspire @@ -14,7 +14,7 @@ COPY Bottomly.ServiceDefaults/ Bottomly.ServiceDefaults/ RUN dotnet publish Bottomly/Bottomly.csproj -c Release -o /app/publish --no-restore -FROM mcr.microsoft.com/dotnet/nightly/runtime:10.0 +FROM mcr.microsoft.com/dotnet/runtime:10.0 WORKDIR /app COPY --from=build /app/publish . From c879267ea8f1b2040f63c622a3320df2aaf1ae02 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:10:41 +0000 Subject: [PATCH 06/22] Add .dockerignore to exclude bin/obj from Docker build context Without this, local Windows build artifacts (obj/project.assets.json) were copied into the container, overwriting the clean restore and causing a failure due to a Windows-only NuGet fallback folder path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5967294 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +**/bin/ +**/obj/ From 0de31c31e97418f3a49e3be8e5a785e68950cce7 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:12:56 +0000 Subject: [PATCH 07/22] Build Docker image tagged as 'bottomly' in CI workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 73a9801..e47d7af 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -29,3 +29,6 @@ jobs: - name: Test run: dotnet test bottomly.net.slnx --no-build --configuration Release --verbosity normal + + - name: Build Docker image + run: docker build -t bottomly . From 37aed8b934e6ef84003fede77b9b2f79be73c77d Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:22:44 +0000 Subject: [PATCH 08/22] Put LLM integration behind EnableLlm feature flag (default: disabled) - Add EnableLlm bool to BottomlyOptions (defaults false) - Read EnableLlm from configuration in Program.cs - Wrap all Ollama/LLM DI registrations in EnableLlm guard - Exclude ConversationMessageHandler from auto-registration when disabled - Set EnableLlm: false in appsettings.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Bottomly/Configuration/BottomlyOptions.cs | 1 + Bottomly/Program.cs | 44 +++++++++++++---------- Bottomly/appsettings.json | 3 +- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/Bottomly/Configuration/BottomlyOptions.cs b/Bottomly/Configuration/BottomlyOptions.cs index 0afe660..23949a0 100644 --- a/Bottomly/Configuration/BottomlyOptions.cs +++ b/Bottomly/Configuration/BottomlyOptions.cs @@ -10,6 +10,7 @@ public class BottomlyOptions public string GiphyApiKey { get; set; } = string.Empty; public string Environment { get; set; } = "live"; public string GitHubToken { get; set; } = string.Empty; + public bool EnableLlm { get; set; } = false; public bool IsDebug => Environment != "live"; } \ No newline at end of file diff --git a/Bottomly/Program.cs b/Bottomly/Program.cs index b441261..ad7bedc 100644 --- a/Bottomly/Program.cs +++ b/Bottomly/Program.cs @@ -7,6 +7,7 @@ using Bottomly.Slack; using Bottomly.Slack.MembershipEventHandlers; using Bottomly.Slack.MessageEventHandlers; +using Bottomly.Slack.MessageEventHandlers.ConversationMessageHandling; using Bottomly.Slack.ReactionHandlers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -41,6 +42,7 @@ opts.GiphyApiKey = builder.Configuration["bottomly_giphy_api_key"] ?? string.Empty; opts.Environment = builder.Configuration["bottomly_env"] ?? "live"; opts.GitHubToken = builder.Configuration["bottomly_github_token"] ?? string.Empty; + opts.EnableLlm = builder.Configuration.GetValue("EnableLlm"); }); var opts = builder.Services.BuildServiceProvider().GetRequiredService>(); @@ -71,7 +73,9 @@ .RegisterEventHandler()); // Event handlers (registered for IEventHandler collection, excluding Help which is separate) -builder.RegisterEventHandlers(Assembly.GetExecutingAssembly()); +builder.RegisterEventHandlers(Assembly.GetExecutingAssembly(), opts.Value.EnableLlm + ? [] + : [typeof(ConversationMessageHandler)]); // Help handler (also registered as singleton for direct injection into SlackWorker) builder.Services.AddSingleton(); @@ -91,26 +95,29 @@ builder.Services.AddHostedService(sp => sp.GetRequiredService()); // LLM Support -builder.AddOllamaApiClient("bottomlymodel") - .AddChatClient(); +if (opts.Value.EnableLlm) +{ + builder.AddOllamaApiClient("bottomlymodel") + .AddChatClient(); -// builder.Services.AddChatClient( -// new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3.5:4b")); + // builder.Services.AddChatClient( + // new OllamaApiClient(new Uri("http://localhost:11434"), "qwen3.5:4b")); -// The built-in resilience settings are super aggressive, with a 10s timeout. -// Running locally Qwen3 takes ~2m to respond to simple queries, so we need to override the defaults. + // The built-in resilience settings are super aggressive, with a 10s timeout. + // Running locally Qwen3 takes ~2m to respond to simple queries, so we need to override the defaults. #pragma warning disable EXTEXP0001 -builder.Services.AddHttpClient("bottomlymodel_httpClient") - .RemoveAllResilienceHandlers() + builder.Services.AddHttpClient("bottomlymodel_httpClient") + .RemoveAllResilienceHandlers() #pragma warning restore EXTEXP0001 - .AddStandardResilienceHandler(options => - { - options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); - options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(4); - options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(8); - }); - -builder.Services.AddTransient(); + .AddStandardResilienceHandler(options => + { + options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); + options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(4); + options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(8); + }); + + builder.Services.AddTransient(); +} // Seeding builder.Services.AddSingleton(); @@ -134,11 +141,12 @@ public static class HostBuilderExtensions { extension(HostApplicationBuilder builder) { - public void RegisterEventHandlers(Assembly assembly) => + public void RegisterEventHandlers(Assembly assembly, Type[] exclude) => assembly.GetTypes() .Where(t => typeof(IMessageEventHandler).IsAssignableFrom(t) && t is { IsInterface: false, IsAbstract: false }) .Where(t => t.Name != nameof(HelpHandler)) + .Where(t => !exclude.Contains(t)) .ToList() .ForEach(t => builder.Services.AddSingleton(typeof(IMessageEventHandler), t)); diff --git a/Bottomly/appsettings.json b/Bottomly/appsettings.json index ea4dd90..e6e8c1e 100644 --- a/Bottomly/appsettings.json +++ b/Bottomly/appsettings.json @@ -7,5 +7,6 @@ } }, "bottomly_env": "debug", - "ImportMemberSeedData": true + "ImportMemberSeedData": true, + "EnableLlm": false } \ No newline at end of file From 35d2ac3c2e804309f50dbbaf32e541842e70fd7b Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:28:08 +0000 Subject: [PATCH 09/22] Fix Dockerfile: use aspnet runtime image instead of base runtime ServiceDefaults pulls in Microsoft.AspNetCore.App via OpenTelemetry and health check dependencies, requiring the ASP.NET Core runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6b71675..01fc781 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ COPY Bottomly.ServiceDefaults/ Bottomly.ServiceDefaults/ RUN dotnet publish Bottomly/Bottomly.csproj -c Release -o /app/publish --no-restore -FROM mcr.microsoft.com/dotnet/runtime:10.0 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app COPY --from=build /app/publish . From 07a32469df45e5202570aaa46680b9c404820d4a Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:48:15 +0000 Subject: [PATCH 10/22] Fixes bug in karma command handling --- .../Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs | 2 +- .../Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs | 2 +- bottomly.net.slnx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs index 8770bb4..e834fb6 100644 --- a/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs +++ b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs @@ -61,7 +61,7 @@ public async Task HandleAsync_NoRecipient_CallsCommandWithMessageUser() { _mockKarmaRepo.Setup(r => r.GetCurrentNetKarmaAsync("U_sender")).ReturnsAsync(0); - await _handler.HandleAsync(CreateMessage("_karma ")); + await _handler.HandleAsync(CreateMessage("_karma")); _mockKarmaRepo.Verify(r => r.GetCurrentNetKarmaAsync("U_sender"), Times.Once()); } diff --git a/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs b/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs index 70a2daa..466b7f0 100644 --- a/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs +++ b/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs @@ -28,7 +28,7 @@ protected override string GetPurpose() => protected override async Task InvokeHandlerLogicAsync(MessageEvent message) { var text = await parser.ReplaceSlackIdTokensWithUsernamesAsync(message.Text!); - var recipient = text[CommandTrigger.Length..].Split(' ')[0]; + var recipient = text[CommandTrigger.Trim().Length..].Split(' ').FirstOrDefault(string.Empty); if (string.IsNullOrEmpty(recipient)) { recipient = message.User; diff --git a/bottomly.net.slnx b/bottomly.net.slnx index 87d8a19..27c5f8a 100644 --- a/bottomly.net.slnx +++ b/bottomly.net.slnx @@ -3,6 +3,7 @@ + From 3df18412083b8cd22ea2072f6d2d8ecac4585878 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:49:42 +0000 Subject: [PATCH 11/22] Additional test for karma handler bug --- .../Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs index e834fb6..c0da8a1 100644 --- a/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs +++ b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs @@ -56,12 +56,14 @@ public async Task HandleAsync_PlainRecipient_CallsCommandWithRecipient() _mockKarmaRepo.Verify(r => r.GetCurrentNetKarmaAsync("alice"), Times.Once()); } - [Fact] - public async Task HandleAsync_NoRecipient_CallsCommandWithMessageUser() + [Theory] + [InlineData("_karma")] + [InlineData("_karma ")] + public async Task HandleAsync_NoRecipient_CallsCommandWithMessageUser(string message) { _mockKarmaRepo.Setup(r => r.GetCurrentNetKarmaAsync("U_sender")).ReturnsAsync(0); - await _handler.HandleAsync(CreateMessage("_karma")); + await _handler.HandleAsync(CreateMessage(message)); _mockKarmaRepo.Verify(r => r.GetCurrentNetKarmaAsync("U_sender"), Times.Once()); } From 0987877222b96292c84321f1355251138c39d85a Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:51:41 +0000 Subject: [PATCH 12/22] Update deployment workflows for .NET Dockerfile - Remove -f run.dockerfile flag, use default Dockerfile - Replace bottomly_mongo_conn_str with ConnectionStrings__mongodb to match Aspire connection string conventions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/push_to_live.yml | 4 ++-- .github/workflows/push_to_test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/push_to_live.yml b/.github/workflows/push_to_live.yml index 7a122ab..e472479 100644 --- a/.github/workflows/push_to_live.yml +++ b/.github/workflows/push_to_live.yml @@ -23,7 +23,7 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - run: | - docker build . -f run.dockerfile -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - name: Deploy to Azure Container Instances uses: azure/aci-deploy@v1 @@ -43,5 +43,5 @@ jobs: bottomly_slack_app_token=${{ secrets.SLACK_APP_TOKEN }} bottomly_prefix=${{ secrets.PREFIX }} bottomly_env=${{ secrets.ENV }} - bottomly_mongo_conn_str=${{ secrets.MONGO_CONN_STR }} + ConnectionStrings__mongodb=${{ secrets.MONGO_CONN_STR }} bottomly_github_token=${{ secrets.RELEASE_HISTORY_TOKEN }} diff --git a/.github/workflows/push_to_test.yml b/.github/workflows/push_to_test.yml index fc00429..234cace 100644 --- a/.github/workflows/push_to_test.yml +++ b/.github/workflows/push_to_test.yml @@ -24,7 +24,7 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - run: | - docker build . -f run.dockerfile -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - name: Deploy to Azure Container Instances uses: azure/aci-deploy@v1 @@ -44,5 +44,5 @@ jobs: bottomly_slack_app_token=${{ secrets.SLACK_APP_TOKEN }} bottomly_prefix=${{ secrets.PREFIX }} bottomly_env=${{ secrets.ENV }} - bottomly_mongo_conn_str=${{ secrets.MONGO_CONN_STR }} + ConnectionStrings__mongodb=${{ secrets.MONGO_CONN_STR }} bottomly_github_token=${{ secrets.RELEASE_HISTORY_TOKEN }} From db71290ebbb0579e9f1495087db92d35fa1f750b Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:52:35 +0000 Subject: [PATCH 13/22] Updates deployment workflows to use new dockerfile --- .github/workflows/build_and_test.yml | 68 ---------------------------- 1 file changed, 68 deletions(-) delete mode 100644 .github/workflows/build_and_test.yml diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml deleted file mode 100644 index 9681a07..0000000 --- a/.github/workflows/build_and_test.yml +++ /dev/null @@ -1,68 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Build & Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - inputs: - logLevel: - description: 'Log level' - required: true - default: 'warning' - type: choice - options: - - info - - warning - - debug - tags: - description: 'Test scenario tags' - required: false - type: boolean - environment: - description: 'Environment to run tests against' - type: environment - required: true - -jobs: - build: - - runs-on: ubuntu-latest - - environment: - name: test - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.11.4 - uses: actions/setup-python@v2 - with: - python-version: 3.11.4 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - env: - bottomly_giphy_api_key: ${{ secrets.GIPHY_API_KEY }} - bottomly_google_api_key: ${{ secrets.GOOGLE_API_KEY }} - bottomly_google_cse_id: ${{ secrets.GOOGLE_CSE_ID }} - bottomly_slack_bot_token: ${{ secrets.SLACK_TOKEN }} - bottomly_slack_app_token: ${{ secrets.SLACK_APP_TOKEN }} - bottomly_prefix: _ - bottomly_env: test - bottomly_mongo_conn_str: ${{ secrets.MONGO_CONN_STR }} - bottomly_github_token: ${{ secrets.RELEASE_HISTORY_TOKEN }} - run: | - pytest From 3e84349eb8235d2a6d8c805928f1ace7c2a1ba91 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 10:56:30 +0000 Subject: [PATCH 14/22] Fix CI: add --include-previews to workload install and fix NuGet.config keys - dotnet workload install aspire requires --include-previews to install the preview Aspire workload manifest in CI - NuGet.config source keys were set to URLs instead of friendly names, which can cause packageSourceMapping to fail to match in CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/dotnet.yml | 2 +- .nuget/NuGet.config | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index e47d7af..295e83b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: dotnet-version: '10.0.x' - name: Install Aspire workload - run: dotnet workload install aspire + run: dotnet workload install aspire --include-previews - name: Restore dependencies run: dotnet restore bottomly.net.slnx diff --git a/.nuget/NuGet.config b/.nuget/NuGet.config index 2159df8..65534eb 100644 --- a/.nuget/NuGet.config +++ b/.nuget/NuGet.config @@ -2,15 +2,15 @@ - - + - + - + From b1900dbb4a467ded9bd046d8f4ff9b8a2c58ee33 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 11:52:20 +0000 Subject: [PATCH 15/22] Fixes more karma spacing bugs --- .github/workflows/dotnet.yml | 7 ++----- .nuget/NuGet.config | 2 +- Bottomly.Tests/Bottomly.Tests.csproj | 1 + .../Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs | 8 +++++--- .../MessageEventHandlers/GetCurrentNetKarmaHandler.cs | 8 +++++++- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 295e83b..f67a891 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -2,9 +2,9 @@ name: .NET Build and Test on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: build-and-test: @@ -18,9 +18,6 @@ jobs: with: dotnet-version: '10.0.x' - - name: Install Aspire workload - run: dotnet workload install aspire --include-previews - - name: Restore dependencies run: dotnet restore bottomly.net.slnx diff --git a/.nuget/NuGet.config b/.nuget/NuGet.config index 65534eb..c9af859 100644 --- a/.nuget/NuGet.config +++ b/.nuget/NuGet.config @@ -8,7 +8,7 @@ - + diff --git a/Bottomly.Tests/Bottomly.Tests.csproj b/Bottomly.Tests/Bottomly.Tests.csproj index 3d5b464..4027a65 100644 --- a/Bottomly.Tests/Bottomly.Tests.csproj +++ b/Bottomly.Tests/Bottomly.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs index c0da8a1..ee54ab2 100644 --- a/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs +++ b/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs @@ -3,10 +3,11 @@ using Bottomly.Slack.MessageEventHandlers; using Bottomly.Slack.ReactionHandlers; using Bottomly.Tests.Helpers; -using Microsoft.Extensions.Logging.Abstractions; +using Meziantou.Extensions.Logging.Xunit; using Moq; using Shouldly; using SlackNet.Events; +using Xunit.Abstractions; namespace Bottomly.Tests.Slack.EventHandlers; @@ -17,7 +18,7 @@ public class GetCurrentNetKarmaHandlerTests private readonly Mock _mockKarmaRepo = new(); private readonly Mock _mockMemberRepo = new(); - public GetCurrentNetKarmaHandlerTests() + public GetCurrentNetKarmaHandlerTests(ITestOutputHelper outputHelper) { var options = TestHelpers.CreateOptions(); var parser = new SlackParser(_mockMemberRepo.Object); @@ -25,7 +26,7 @@ public GetCurrentNetKarmaHandlerTests() _mockKarmaRepo.Object, new KarmaReactionMap(), parser, _mockBroker.Object, options, - NullLogger.Instance); + XUnitLogger.CreateLogger(outputHelper)); } private static MessageEvent CreateMessage(string text, string user = "U_sender") => @@ -54,6 +55,7 @@ public async Task HandleAsync_PlainRecipient_CallsCommandWithRecipient() await _handler.HandleAsync(CreateMessage("_karma alice")); _mockKarmaRepo.Verify(r => r.GetCurrentNetKarmaAsync("alice"), Times.Once()); + _mockBroker.Verify(b => b.SendMessageAsync("alice: 3", "C1", null), Times.Once()); } [Theory] diff --git a/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs b/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs index 466b7f0..ecec10c 100644 --- a/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs +++ b/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs @@ -28,7 +28,7 @@ protected override string GetPurpose() => protected override async Task InvokeHandlerLogicAsync(MessageEvent message) { var text = await parser.ReplaceSlackIdTokensWithUsernamesAsync(message.Text!); - var recipient = text[CommandTrigger.Trim().Length..].Split(' ').FirstOrDefault(string.Empty); + var recipient = ParseRecipient(text); if (string.IsNullOrEmpty(recipient)) { recipient = message.User; @@ -37,4 +37,10 @@ protected override async Task InvokeHandlerLogicAsync(MessageEvent message) var result = await repository.GetCurrentNetKarmaAsync(recipient); await SendMessageResponseAsync($"{recipient}: {result}", message); } + + private string ParseRecipient(string text) => + text[CommandTrigger.Trim().Length..] // Remove the command trigger + .Trim() // Remove any leading/trailing whitespace + .Split(' ') // Split on whitespace + .FirstOrDefault(string.Empty); // Return the first non-whitespace token } \ No newline at end of file From 70cc2baf348555729aa6b769c061bf092e233a7b Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 11:55:33 +0000 Subject: [PATCH 16/22] Fix NuGet feed: use dotnet10 feed for Aspire preview packages The dotnet9 feed does not carry .NET 10 Aspire preview packages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .nuget/NuGet.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nuget/NuGet.config b/.nuget/NuGet.config index c9af859..91cbf5d 100644 --- a/.nuget/NuGet.config +++ b/.nuget/NuGet.config @@ -3,7 +3,7 @@ + value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json"/> From cea9d9d46ee90ff1c066d8160a00f2bd4e167a8a Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 12:03:57 +0000 Subject: [PATCH 17/22] Fix NuGet feed: revert to dotnet9 feed for Aspire preview packages Aspire.Hosting 13.3.0-preview.1.26156.8 is published to the dotnet9 feed, not dotnet10. Also reverts AppHost package versions back to the correct 13.3.0-preview.1.26156.8 after failed downgrade attempts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .nuget/NuGet.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nuget/NuGet.config b/.nuget/NuGet.config index 91cbf5d..c9af859 100644 --- a/.nuget/NuGet.config +++ b/.nuget/NuGet.config @@ -3,7 +3,7 @@ + value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"/> From 24ae1d4438363d3916c7d772708fe3193ddbaae8 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 12:05:06 +0000 Subject: [PATCH 18/22] SAVEPOINT --- bottomly.net.slnx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bottomly.net.slnx b/bottomly.net.slnx index 27c5f8a..7a94481 100644 --- a/bottomly.net.slnx +++ b/bottomly.net.slnx @@ -1,4 +1,9 @@ + + + + + From 495beee0f962ca55fb4cedccd5e5c2700a80ccb7 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 12:06:34 +0000 Subject: [PATCH 19/22] Specifies restore source explicitly --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f67a891..e74d356 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: dotnet-version: '10.0.x' - name: Restore dependencies - run: dotnet restore bottomly.net.slnx + run: dotnet restore bottomly.net.slnx -s https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json - name: Build run: dotnet build bottomly.net.slnx --no-restore --configuration Release From e354d6d2c41127219c9dd4b85ca7b3ba89fad613 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 12:16:17 +0000 Subject: [PATCH 20/22] reverts previous commit --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index e74d356..f67a891 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: dotnet-version: '10.0.x' - name: Restore dependencies - run: dotnet restore bottomly.net.slnx -s https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json + run: dotnet restore bottomly.net.slnx - name: Build run: dotnet build bottomly.net.slnx --no-restore --configuration Release From 56631981b92b908ecfe6d66a7a9e5db6830de185 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 12:20:51 +0000 Subject: [PATCH 21/22] Move NuGet.config to repo root so dotnet CLI discovers it The dotnet CLI searches for NuGet.config in the current directory and parent directories, but does NOT search .nuget/ subdirectories. The file in .nuget/ was only working locally because Visual Studio has its own fallback feed configs. On CI (GitHub Actions), dotnet restore only saw nuget.org and could not find the preview Aspire packages from the dotnet9 Azure DevOps feed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .nuget/NuGet.config => NuGet.config | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .nuget/NuGet.config => NuGet.config (100%) diff --git a/.nuget/NuGet.config b/NuGet.config similarity index 100% rename from .nuget/NuGet.config rename to NuGet.config From 1f4d68620f65262dc68302cfb7e3a45e3faa4f79 Mon Sep 17 00:00:00 2001 From: "Owen.Morgan-Jones" Date: Tue, 17 Mar 2026 12:22:16 +0000 Subject: [PATCH 22/22] Updates ref to Nuget.config in the solution --- bottomly.net.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottomly.net.slnx b/bottomly.net.slnx index 7a94481..31f93d5 100644 --- a/bottomly.net.slnx +++ b/bottomly.net.slnx @@ -7,7 +7,7 @@ - +