Skip to content

Commit

Permalink
sirbot migration continues
Browse files Browse the repository at this point in the history
  • Loading branch information
ovv committed Jan 12, 2021
1 parent cdede00 commit dc10b90
Show file tree
Hide file tree
Showing 18 changed files with 1,132 additions and 11 deletions.
1 change: 0 additions & 1 deletion .env.sample

This file was deleted.

2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ RUN apk add --no-cache tzdata gcc g++ make postgresql-dev build-base git && \
echo "UTC" >> /etc/timezone && \
apk del tzdata

RUN apk add --no-cache libffi-dev git

COPY requirements requirements
RUN pip install -r requirements/development.txt

Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ services:
DATABASE_URL: "postgresql://${USER}:@postgresql:5432/pyslackers_dev"
SLACK_INVITE_TOKEN: "${SLACK_INVITE_TOKEN}"
SLACK_TOKEN: "${SLACK_TOKEN}"
SLACK_SIGNING_SECRET: "${SLACK_SIGNING_SECRET}"
ports:
- "8000:8000"
volumes:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""add sirbot tables
Revision ID: f7f15f30aeee
Revises: dece77ed5036
Create Date: 2021-01-12 20:42:45.644752+00:00
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "f7f15f30aeee"
down_revision = "dece77ed5036"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"slack_messages",
sa.Column("id", sa.Text(), nullable=False),
sa.Column("send_at", sa.DateTime(), nullable=True),
sa.Column("user", sa.Text(), nullable=True),
sa.Column("channel", sa.Text(), nullable=True),
sa.Column("message", sa.Text(), nullable=True),
sa.Column("raw", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_slack_messages_channel", "slack_messages", ["channel", "send_at"], unique=False
)
op.create_index("ix_slack_messages_user", "slack_messages", ["user", "send_at"], unique=False)
op.create_index(
"ix_slack_messages_user_channel",
"slack_messages",
["user", "channel", "send_at"],
unique=False,
)
op.add_column("slack_users", sa.Column("name", sa.Text(), nullable=True))
op.create_index("ix_slack_users_name", "slack_users", ["name"], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("ix_slack_users_name", table_name="slack_users")
op.drop_column("slack_users", "name")
op.drop_index("ix_slack_messages_user_channel", table_name="slack_messages")
op.drop_index("ix_slack_messages_user", table_name="slack_messages")
op.drop_index("ix_slack_messages_channel", table_name="slack_messages")
op.drop_table("slack_messages")
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions pyslackersweb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ class Source(Enum):
"slack_users",
metadata,
sa.Column("id", sa.Text, primary_key=True),
sa.Column("name", sa.Text),
sa.Column("deleted", sa.Boolean),
sa.Column("admin", sa.Boolean),
sa.Column("bot", sa.Boolean),
sa.Column("timezone", sa.Text),
sa.Column("first_seen", sa.DateTime(timezone=True), default=datetime.now),
sa.Index("ix_slack_users_id", "id"),
sa.Index("ix_slack_users_name", "name"),
sa.Index("ix_slack_users_admin", "id", "admin"),
sa.Index("ix_slack_users_timezone", "id", "timezone"),
)
4 changes: 4 additions & 0 deletions pyslackersweb/sirbot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import logging

from aiohttp import web

from .views import routes
from .context import background_jobs

logger = logging.getLogger(__name__)


async def app_factory() -> web.Application:
sirbot = web.Application()
Expand Down
6 changes: 6 additions & 0 deletions pyslackersweb/sirbot/database.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging

import asyncpg
import sqlalchemy as sa

from pyslackersweb import models
from pyslackersweb.util.log import ContextAwareLoggerAdapter


Expand All @@ -15,3 +17,7 @@ async def get_challenge(conn: asyncpg.connection.Connection) -> str:
SELECT id FROM codewars_challenge WHERE posted_at IS NULL ORDER BY RANDOM() LIMIT 1
) RETURNING id""",
)


async def is_admin(conn: asyncpg.connection.Connection, user: str) -> bool:
return await conn.fetchval(sa.select([models.SlackUsers.c.admin]).where(id=user))
39 changes: 39 additions & 0 deletions pyslackersweb/sirbot/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import dataclasses

from datetime import datetime
from typing import Optional
from decimal import Decimal

import sqlalchemy as sa

from sqlalchemy.dialects.postgresql import JSONB

from pyslackersweb.models import metadata

codewars = sa.Table(
Expand All @@ -17,3 +23,36 @@
nullable=True,
),
)


@dataclasses.dataclass(frozen=True)
class StockQuote:
# pylint: disable=too-many-instance-attributes

symbol: str
company: str
price: Decimal
change: Decimal
change_percent: Decimal
market_open: Decimal
market_close: Decimal
high: Decimal
low: Decimal
volume: Decimal
time: datetime
logo: Optional[str] = None


SlackMessage = sa.Table(
"slack_messages",
metadata,
sa.Column("id", sa.Text, primary_key=True),
sa.Column("send_at", sa.DateTime),
sa.Column("user", sa.Text),
sa.Column("channel", sa.Text),
sa.Column("message", sa.Text),
sa.Column("raw", JSONB),
sa.Index("ix_slack_messages_user", "user", "send_at"),
sa.Index("ix_slack_messages_channel", "channel", "send_at"),
sa.Index("ix_slack_messages_user_channel", "user", "channel", "send_at"),
)
14 changes: 11 additions & 3 deletions pyslackersweb/sirbot/settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import os

# production
IS_PRODUCTION = os.environ.get("PLATFORM_BRANCH") == "master"

# production settings
READTHEDOCS_NOTIFICATION_CHANNEL = "community_projects"
SLACK_TEAM_ID = os.environ.get("SLACK_TEAM_ID")
SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "")
SLACK_ADMIN_CHANNEL = os.environ.get("SLACK_ADMIN_CHANNEL", "")
SLACK_INTRODUCTION_CHANNEL = "introductions"

# Development
if os.environ.get("PLATFORM_BRANCH") != "master":
# Development settings
if not IS_PRODUCTION:
READTHEDOCS_NOTIFICATION_CHANNEL = "general"
SLACK_ADMIN_CHANNEL = "CJ1BWMBDX" # general
SLACK_INTRODUCTION_CHANNEL = "general"
188 changes: 188 additions & 0 deletions pyslackersweb/sirbot/slack/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import json
import logging
import asyncio

from aiohttp import web
from slack import methods
from slack import actions
from slack.events import Message
from slack.exceptions import SlackAPIError, RateLimited

from pyslackersweb.sirbot import settings, models
from pyslackersweb.util.log import ContextAwareLoggerAdapter

logger = ContextAwareLoggerAdapter(logging.getLogger(__name__))


async def topic_change_revert(request: web.Request, action: actions.Action) -> None:
response = Message()
response["channel"] = action["channel"]["id"]
response["ts"] = action["message_ts"]
response["attachments"] = action["original_message"]["attachments"]
response["attachments"][0]["color"] = "danger"
response["attachments"][0]["text"] = f'Change reverted by <@{action["user"]["id"]}>'
del response["attachments"][0]["actions"]

data = json.loads(action["actions"][0]["value"])
await request.app["slack_client"].query(
url=methods.CHANNELS_SET_TOPIC,
data={"channel": data["channel"], "topic": data["old_topic"]},
)

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def topic_change_validate(request: web.Request, action: actions.Action) -> None:
response = Message()
response["channel"] = action["channel"]["id"]
response["ts"] = action["message_ts"]
response["attachments"] = action["original_message"]["attachments"]
response["attachments"][0]["color"] = "good"
response["attachments"][0]["text"] = f'Change validated by <@{action["user"]["id"]}>'
del response["attachments"][0]["actions"]

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def purpose_change_revert(request: web.Request, action: actions.Action) -> None:
response = Message()
response["channel"] = action["channel"]["id"]
response["ts"] = action["message_ts"]
response["attachments"] = action["original_message"]["attachments"]
response["attachments"][0]["color"] = "danger"
response["attachments"][0]["text"] = f'Change reverted by <@{action["user"]["id"]}>'
del response["attachments"][0]["actions"]

data = json.loads(action["actions"][0]["value"])
await request.app["slack_client"].query(
url=methods.CHANNELS_SET_PURPOSE,
data={"channel": data["channel"], "purpose": data["old_purpose"]},
)

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def purpose_change_validate(request: web.Request, action: actions.Action) -> None:
response = Message()
response["channel"] = action["channel"]["id"]
response["ts"] = action["message_ts"]
response["attachments"] = action["original_message"]["attachments"]
response["attachments"][0]["color"] = "good"
response["attachments"][0]["text"] = f'Change validated by <@{action["user"]["id"]}>'
del response["attachments"][0]["actions"]

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def pin_added_validate(request: web.Request, action: actions.Action) -> None:
response = Message()
response["channel"] = action["channel"]["id"]
response["ts"] = action["message_ts"]
response["attachments"] = action["original_message"]["attachments"]
response["attachments"][0]["color"] = "good"
response["attachments"][0]["pretext"] = f'Pin validated by <@{action["user"]["id"]}>'
del response["attachments"][0]["actions"]

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def pin_added_revert(request: web.Request, action: actions.Action) -> None:
response = Message()

response["channel"] = action["channel"]["id"]
response["ts"] = action["message_ts"]
response["attachments"] = action["original_message"]["attachments"]
response["attachments"][0]["color"] = "danger"
response["attachments"][0]["pretext"] = f'Pin reverted by <@{action["user"]["id"]}>'
del response["attachments"][0]["actions"]

action_data = json.loads(action["actions"][0]["value"])
remove_data = {"channel": action_data["channel"]}

if action_data["item_type"] == "message":
remove_data["timestamp"] = action_data["item_id"]
elif action_data["item_type"] == "file":
remove_data["file"] = action_data["item_id"]
elif action_data["item_type"] == "file_comment":
remove_data["file_comment"] = action_data["item_id"]
else:
raise TypeError(f'Unknown pin type: {action_data["type"]}')

try:
await request.app["slack_client"].query(url=methods.PINS_REMOVE, data=remove_data)
except SlackAPIError as e:
if e.error != "no_pin":
raise

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def admin_msg(request: web.Request, action: actions.Action) -> None:
admin_msg = Message()
admin_msg["channel"] = settings.SLACK_ADMIN_CHANNEL
admin_msg["attachments"] = [
{
"fallback": f'Message from {action["user"]["name"]}',
"title": f'Message from <@{action["user"]["id"]}>',
"color": "good",
"text": action["submission"]["message"],
}
]

await request.app["slack_client"].query(url=methods.CHAT_POST_MESSAGE, data=admin_msg)

response = Message()
response["response_type"] = "ephemeral"
response["text"] = "Thank you for your message."

await request.app["slack_client"].query(url=action["response_url"], data=response)


async def user_cleanup(request: web.Request, action: actions.Action) -> None:
user_id = action["actions"][0]["value"]

response = Message()
response["text"] = f"Cleanup of <@{user_id}> triggered by <@{action['user']['id']}>"

await request.app["slack_client"].query(url=action["response_url"], data=response)

asyncio.create_task(_cleanup_user(request.app, user_id))


async def _cleanup_user(app: web.Application, user: str) -> None:
try:
async with app["pg"].acquire() as conn:
messages = await conn.fetch(
"""SELECT id, channel FROM slack_messages WHERE "user" = $1 ORDER BY send_at DESC""",
user,
)

for message in messages:
await _delete_message(app["slack_client"], message)
except Exception:
logger.exception("Unexpected exception cleaning up user %s", user)


async def _delete_message(slack, message: dict) -> None:
data = {"channel": message["channel"], "ts": message["id"]}
try:
await slack.query(url=methods.CHAT_DELETE, data=data)
except RateLimited:
logger.debug("sleeping")
await asyncio.sleep(20)
await _delete_message(slack, message)
except SlackAPIError as e:
if e.error == "message_not_found":
return
else:
logger.exception(
"Failed to cleanup message %s in channel %s",
message["id"],
message["channel"],
)
except Exception:
logger.exception(
"Failed to cleanup message %s in channel %s",
message["id"],
message["channel"],
)
Loading

0 comments on commit dc10b90

Please sign in to comment.