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/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5967294 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +**/bin/ +**/obj/ diff --git a/bottomly.net/.editorconfig b/.editorconfig similarity index 100% rename from bottomly.net/.editorconfig rename to .editorconfig 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 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3188b43..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,14 +18,14 @@ jobs: with: dotnet-version: '10.0.x' - - name: Install Aspire workload - 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 + + - name: Build Docker image + run: docker build -t bottomly . 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 }} 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/.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 92% rename from bottomly.net/Bottomly.Tests/Bottomly.Tests.csproj rename to Bottomly.Tests/Bottomly.Tests.csproj index 3d5b464..4027a65 100644 --- a/bottomly.net/Bottomly.Tests/Bottomly.Tests.csproj +++ b/Bottomly.Tests/Bottomly.Tests.csproj @@ -10,6 +10,7 @@ + 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 85% rename from bottomly.net/Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs rename to Bottomly.Tests/Slack/EventHandlers/GetCurrentNetKarmaHandlerTests.cs index 8770bb4..ee54ab2 100644 --- a/bottomly.net/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,14 +55,17 @@ 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()); } - [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()); } 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 92% rename from bottomly.net/Bottomly/Configuration/BottomlyOptions.cs rename to Bottomly/Configuration/BottomlyOptions.cs index 0afe660..23949a0 100644 --- a/bottomly.net/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.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 79% rename from bottomly.net/Bottomly/Program.cs rename to Bottomly/Program.cs index b441261..ad7bedc 100644 --- a/bottomly.net/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.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 79% rename from bottomly.net/Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs rename to Bottomly/Slack/MessageEventHandlers/GetCurrentNetKarmaHandler.cs index 70a2daa..ecec10c 100644 --- a/bottomly.net/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 = 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 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 75% rename from bottomly.net/Bottomly/appsettings.json rename to Bottomly/appsettings.json index ea4dd90..e6e8c1e 100644 --- a/bottomly.net/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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..01fc781 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/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/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . + +ENTRYPOINT ["dotnet", "Bottomly.dll"] diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..c9af859 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 6b77970..fa05186 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,81 @@ # 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 +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 +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/bottomly.net/bottomly.net.slnx b/bottomly.net.slnx similarity index 57% rename from bottomly.net/bottomly.net.slnx rename to bottomly.net.slnx index 87d8a19..31f93d5 100644 --- a/bottomly.net/bottomly.net.slnx +++ b/bottomly.net.slnx @@ -1,8 +1,14 @@ + + + + + - + + 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 diff --git a/bottomly.net/.nuget/NuGet.config b/bottomly.net/.nuget/NuGet.config deleted file mode 100644 index 2159df8..0000000 --- a/bottomly.net/.nuget/NuGet.config +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file 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