Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
types: [ opened, synchronize, reopened ]
paths-ignore:
- '**.md'

jobs:
lint:
name: lint
Expand All @@ -31,6 +32,28 @@ jobs:
- name: lint
run: make lint

security:
name: security
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2

- name: setup python
uses: actions/setup-python@v2
with:
python-version: '3.9'

- name: setup env
run: cp .env.sample .env

- name: install dependencies
run: make setup_dev

- name: bandit
run: make bandit-ci


test:
name: tests
runs-on: ubuntu-latest
Expand Down
24 changes: 18 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ export
###################
# Setup tasks #
###################
install_dev:
pip install -e ".[dev]"

install:
pip install .

setup_venv:
python3.9 -m venv venv
. venv/bin/activate
pip install .

setup_dev: setup_venv
pip install -e ".[dev]"
setup_dev: setup_venv install_dev

setup:
pip3 install .
setup: setup_venv install

###################
# Testing #
Expand All @@ -26,7 +29,16 @@ lint:
mypy main.py app tests

cover:
coverage run --source=app -m pytest -x .
coverage run --source=app -m pytest -xv .

coverage-report: cover
coverage report -m

bandit:
bandit -r app main.py

bandit-ci:
bandit -r -ll -ii app main.py

test-all: lint cover

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

![CI Status](https://github.com/devcherchecollegue-org/devguy-python/actions/workflows/main.yaml/badge.svg?branch=main)
[![Coverage Status](https://coveralls.io/repos/github/devcherchecollegue-org/devguy-python/badge.svg?branch=main)](https://coveralls.io/github/devcherchecollegue-org/devguy-python?branch=main)
[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)

<!-- TOC -->

Expand Down
27 changes: 27 additions & 0 deletions app/core/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pydantic import BaseModel
from discord import Guild, colour
from typing import Optional, List


class Role(BaseModel):
color: Optional[str] = None
name: str

async def create(self, guild: Guild):
if self.color:
return guild.create_role(
name=self.name,
colour=colour.Colour(f"0x{self.color}"),
)

return guild.create_role(name=self.name)


def requires_role(
role: Role, in_roles: List[Role], err: Exception = Exception("forbiden")
):
if all(role.name != candidate.name for candidate in in_roles):
raise err


ADMIN = Role(name="devguy_admin")
3 changes: 3 additions & 0 deletions app/core/sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from sqlalchemy.orm import declarative_base

Base = declarative_base()
1 change: 1 addition & 0 deletions app/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
deps = {}
12 changes: 4 additions & 8 deletions app/discord/bot.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
from app.discord import client

# flake8: noqa
from app.discord import messages # load messages
from app.discord import messages, events

# This file cannot be tested through unit testing cause it requires to start a true discord application.
# It could be done through dependencie injection using a mocked discord instance then starting though but
# it does not have much sense testing it as it only logs some information for bot startup.


def start(token: str): # pragma: no-cover
events.Events(None) # TODO: inject messenger
messages.Message(None)
client.run(token)


@client.event
@client.event # pragma: no-cover
async def on_ready(): # pragma: no-cover
print(f"We have logged in as {client.user}")


@client.event
async def on_connect(): # pragma: no-cover
print("connected")
15 changes: 15 additions & 0 deletions app/discord/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from discord import User

from app.discord import client
from app.modules.messaging import Messenger


class Events:
def __init__(self, messenger: Messenger):
self.__messenger = messenger

@client.event
async def on_member_join(self, member: User):
msg, channel_id = self.__messenger.welcome(member.name)
chan = client.get_channel(channel_id)
await chan.send(msg)
38 changes: 27 additions & 11 deletions app/discord/messages.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
from discord import Message
from discord import Client
from app.discord import client
from app.modules import hello
from app.modules.messaging import Messenger
from discord import Message


class Messages:
def __init__(self, messenger: Messenger, prefix: str = "!devguy"):
self.__messenger = messenger
self.__prefix = prefix

def __is_cmd(self, message: str):
if message.startswith(self.__prefix):
splitted = message.split(" ")
return True, splitted[0], splitted[1:]

return False, None

@client.event
async def on_message(self, message: Message):
is_cmd, cmd, args = self.__is_cmd(message.content)
if not is_cmd:
return

if cmd == "set_as_welcome_channel":
self.__messenger.set_welcome_channel(message.channel.id, message.author)
return

@client.event
async def on_message(
message: Message,
usecase: hello.Usecase = hello.Usecase(),
cli: Client = client,
):
if message.content.startswith("hello"):
await usecase.respond(message, cli.user.name)
if cmd == "set_welcome_message":
self.__messenger.set_welcome_message(" ".join(args), message.author)
return
11 changes: 0 additions & 11 deletions app/modules/hello.py

This file was deleted.

3 changes: 3 additions & 0 deletions app/modules/messaging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .usecases import Messenger

__all__ = ["Messenger"]
57 changes: 57 additions & 0 deletions app/modules/messaging/_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from abc import abstractmethod, ABC
from app.core.sqlalchemy import Base
from .api import Kind, Validator, Messages
from string import Formatter
from sqlalchemy.orm import Column, Integer, Varchar, Datetime
from datetime import datetime


class _Validator(Validator):
def is_welcome(self, message: str) -> bool:
args = [tup[1] for tup in Formatter().parse(message) if tup[1] is not None]
return args == self.WELCOME_ARGS


class _MessageEntity(Base):
__tablename__ = "messages"

id = Column(Integer, primary_key="true", autoincrement=True)
kind = Column(Integer, nullable=False)
content = Column(Varchar(500), nullable=False)

created_at = Column(Datetime, nullable=False, default=datetime.utcnow)
last_updated_at = Column(Datetime, nullable=False, default=datetime.utcnow)


class _MessageQueries(ABC):
@abstractmethod
def insert(self, message: _MessageEntity):
"""Insert message into DB"""

@abstractmethod
def update(self, message: _MessageEntity):
"""Update message into DB"""

@abstractmethod
def get(self, message_kind: Kind):
"""Get current message for provided kind"""


class _Messages(Messages):
def __init__(self, querier: _MessageQueries):
self.__query = querier

def save(self, kind: Kind, content: str):
msg = None
try:
msg = self.__query.get(kind)
except Exception as e:
print(e)
return False

if not msg:
msg = _MessageEntity(kind=kind.value, content=content)
return self.__query.insert(msg)

msg.content = content
return self.__query.update(msg)
Empty file.
47 changes: 47 additions & 0 deletions app/modules/messaging/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Optional


class Kind(Enum):
WELCOME = 0


class Validator(ABC):
WELCOME_ARGS = ["new_member_name"]

@abstractmethod
def is_welcome(self, message: str) -> bool:
"""
Ensure provided message is a correctly
formatted welcome one.
"""


class Messages(ABC):
@abstractmethod
def save(self, kind: Kind, msg: str) -> bool:
"""
Store message with correct kind.
Variables has to be passed using the
python format syntax.
"""

@abstractmethod
def welcome(self) -> str:
"""
Retrieve stored welcome message if exits.
"""


class Channels(ABC):
@abstractmethod
def save(self, kind: Kind, id: int) -> bool:
"""
Store channel id with correct kind.
Those channel will be used for announcement.
"""

@abstractmethod
def welcome(self) -> Optional[int]:
"""Retrieve stored channel for welcome message"""
54 changes: 54 additions & 0 deletions app/modules/messaging/usecases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from .api import Kind, Messages, Validator, Channels
from discord.member import Member
from app.core import roles
from typing import Tuple


class Messenger:
class InvalidMessage(Exception):
def __init__(self, kind: Kind):
super().__init__(f"candidate is not a valid {kind.name} message.")

class ForbidenAction(Exception):
pass

class NoChannelDefined(Exception):
def __init__(self, kind: Kind):
super().__init__(f"no channel define to announce {kind.name}.")

def __init__(self, messages: Messages, channels: Channels, validator: Validator):
self.__messages = messages
self.__validator = validator
self.__channels = channels

def set_welcome_channel(self, channel_id: int, user: Member) -> None:
roles.requires_role(roles.ADMIN, user.roles, self.ForbidenAction)

if not self.__channels.save(Kind.WELCOME, channel_id):
raise Exception("could not save welcome channel")

def set_welcome_message(self, message: str, user: Member) -> None:
roles.requires_role(roles.ADMIN, user.roles, self.ForbidenAction)

if not self.__validator.is_welcome(message):
raise self.InvalidMessage(Kind.WELCOME)

if not self.__messages.save(Kind.WELCOME, message):
raise Exception("could not save welcome message")

def welcome(self, new_member_name: str) -> Tuple[str, int]:
"""Welcome a new user"""
channel_id = self.__channels.welcome()
if not channel_id:
raise self.NoChannelDefined(Kind.WELCOME)

msg = self.__messages.welcome()
if not msg:
msg = (
f"Hello {{{self.__validator.WELCOME_ARGS[0]}}} et bienvenue dans "
"la communauté! Présente toi ma gueule ;) !"
)

format_args = {self.__validator.WELCOME_ARGS[0]: new_member_name}

return (msg.format(**format_args), channel_id)
3 changes: 2 additions & 1 deletion config.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[bot]
token= %(discord_token)s
token= %(discord_token)s
prefix= "!devguy "
Loading