Skip to content

Commit 1120bb2

Browse files
committed
Sirbot
1 parent f13395f commit 1120bb2

28 files changed

+1436
-172
lines changed

Diff for: .env.sample

-1
This file was deleted.

Diff for: Dockerfile

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ RUN apk add --no-cache tzdata gcc g++ make postgresql-dev build-base && \
1111
echo "UTC" >> /etc/timezone && \
1212
apk del tzdata
1313

14+
RUN apk add --no-cache libffi-dev git
15+
1416
COPY requirements requirements
1517
RUN pip install -r requirements/development.txt
1618

Diff for: docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
DATABASE_URL: "postgresql://${USER}:@postgresql:5432/pyslackers_dev"
1313
SLACK_INVITE_TOKEN: "${SLACK_INVITE_TOKEN}"
1414
SLACK_TOKEN: "${SLACK_TOKEN}"
15+
SLACK_SIGNING_SECRET: "${SLACK_SIGNING_SECRET}"
1516
ports:
1617
- "8000:8000"
1718
volumes:
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""empty message
2+
3+
Revision ID: 5b03cd946e60
4+
Revises: 3df042324a1f
5+
Create Date: 2020-03-01 20:33:03.352360+00:00
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "5b03cd946e60"
14+
down_revision = "3df042324a1f"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table(
22+
"slack_channels",
23+
sa.Column("id", sa.Text(), nullable=False),
24+
sa.Column("name", sa.Text(), nullable=True),
25+
sa.Column("created", sa.DateTime(timezone=True), nullable=True),
26+
sa.Column("archived", sa.Boolean(), nullable=True),
27+
sa.Column("members", sa.Integer(), nullable=True),
28+
sa.Column("topic", sa.Text(), nullable=True),
29+
sa.Column("purpose", sa.Text(), nullable=True),
30+
sa.PrimaryKeyConstraint("id"),
31+
sa.UniqueConstraint("name"),
32+
)
33+
op.create_index("ix_slack_channels_id", "slack_channels", ["id"], unique=False)
34+
op.create_index("ix_slack_channels_name", "slack_channels", ["name"], unique=False)
35+
36+
op.create_table(
37+
"slack_messages",
38+
sa.Column("id", sa.Text(), nullable=False),
39+
sa.Column("send_at", sa.DateTime(), nullable=True),
40+
sa.Column("user", sa.Text(), nullable=True),
41+
sa.Column("channel", sa.Text(), nullable=True),
42+
sa.Column("message", sa.Text(), nullable=True),
43+
sa.Column("raw", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
44+
sa.PrimaryKeyConstraint("id"),
45+
)
46+
op.create_index(
47+
"ix_slack_messages_channel", "slack_messages", ["channel", "send_at"], unique=False
48+
)
49+
op.create_index("ix_slack_messages_user", "slack_messages", ["user", "send_at"], unique=False)
50+
op.create_index(
51+
"ix_slack_messages_user_channel",
52+
"slack_messages",
53+
["user", "channel", "send_at"],
54+
unique=False,
55+
)
56+
57+
op.create_table(
58+
"slack_users",
59+
sa.Column("id", sa.Text(), nullable=False),
60+
sa.Column("name", sa.Text(), nullable=True),
61+
sa.Column("deleted", sa.Boolean(), nullable=True),
62+
sa.Column("admin", sa.Boolean(), nullable=True),
63+
sa.Column("bot", sa.Boolean(), nullable=True),
64+
sa.Column("timezone", sa.Text(), nullable=True),
65+
sa.Column("first_seen", sa.DateTime(timezone=True), nullable=True),
66+
sa.PrimaryKeyConstraint("id"),
67+
sa.UniqueConstraint("name"),
68+
)
69+
op.create_index("ix_slack_users_admin", "slack_users", ["id", "admin"], unique=False)
70+
op.create_index("ix_slack_users_id", "slack_users", ["id"], unique=False)
71+
op.create_index("ix_slack_users_name", "slack_users", ["id", "name"], unique=False)
72+
op.create_index("ix_slack_users_timezone", "slack_users", ["id", "timezone"], unique=False)
73+
# ### end Alembic commands ###
74+
75+
76+
def downgrade():
77+
# ### commands auto generated by Alembic - please adjust! ###
78+
op.drop_index("ix_slack_users_timezone", table_name="slack_users")
79+
op.drop_index("ix_slack_users_name", table_name="slack_users")
80+
op.drop_index("ix_slack_users_id", table_name="slack_users")
81+
op.drop_index("ix_slack_users_admin", table_name="slack_users")
82+
op.drop_table("slack_users")
83+
84+
op.drop_index("ix_slack_messages_user_channel", table_name="slack_messages")
85+
op.drop_index("ix_slack_messages_user", table_name="slack_messages")
86+
op.drop_index("ix_slack_messages_channel", table_name="slack_messages")
87+
op.drop_table("slack_messages")
88+
89+
op.drop_index("ix_slack_channels_name", table_name="slack_channels")
90+
op.drop_index("ix_slack_channels_id", table_name="slack_channels")
91+
op.drop_table("slack_channels")
92+
# ### end Alembic commands ###

Diff for: pyslackersweb/__init__.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
from sentry_sdk.integrations.logging import LoggingIntegration
1010
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
1111

12-
from .contexts import apscheduler, client_session, redis_pool, postgresql_pool, slack_client
12+
from .contexts import (
13+
apscheduler,
14+
client_session,
15+
redis_pool,
16+
postgresql_pool,
17+
slack_client,
18+
background_jobs,
19+
)
1320
from .middleware import request_context_middleware
1421
from . import settings, website, sirbot
1522

@@ -58,7 +65,9 @@ async def app_factory(*args) -> web.Application: # pylint: disable=unused-argum
5865
SLACK_TOKEN=settings.SLACK_TOKEN,
5966
)
6067

61-
app.cleanup_ctx.extend([apscheduler, client_session, redis_pool, postgresql_pool, slack_client])
68+
app.cleanup_ctx.extend(
69+
[apscheduler, client_session, redis_pool, postgresql_pool, slack_client, background_jobs]
70+
)
6271

6372
app.router.add_get("/", index)
6473

Diff for: pyslackersweb/contexts.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
from typing import AsyncGenerator
2-
31
import json
2+
import datetime
3+
import logging
4+
5+
from typing import AsyncGenerator
46

57
import aioredis
68
import asyncpgsa
9+
import asyncpg.pool
10+
import sqlalchemy as sa
711

812
from asyncpgsa.connection import get_dialect
913
from aiohttp import ClientSession, web
1014
from apscheduler.schedulers.asyncio import AsyncIOScheduler
1115
from slack.io.aiohttp import SlackAPI
16+
from sqlalchemy import select
17+
18+
from pyslackersweb.util.log import ContextAwareLoggerAdapter
19+
from . import tasks, models
20+
21+
22+
logger = ContextAwareLoggerAdapter(logging.getLogger(__name__))
1223

1324

1425
def _register_in_app(app: web.Application, name: str, item) -> None:
@@ -58,3 +69,41 @@ async def slack_client(app: web.Application) -> AsyncGenerator[None, None]:
5869
_register_in_app(app, "slack_client_legacy", slack_legacy)
5970

6071
yield
72+
73+
74+
async def background_jobs(app: web.Application) -> AsyncGenerator[None, None]:
75+
scheduler = app["scheduler"]
76+
pg: asyncpg.pool.Pool = app["pg"]
77+
slack_client_: SlackAPI = app["slack_client"]
78+
79+
next_run_time = None
80+
if await _is_empty_table(pg, models.SlackUsers.c.id):
81+
next_run_time = datetime.datetime.now() + datetime.timedelta(minutes=1)
82+
83+
scheduler.add_job(
84+
tasks.sync_slack_users,
85+
"cron",
86+
minute=0,
87+
args=(slack_client_, pg),
88+
next_run_time=next_run_time,
89+
)
90+
91+
next_run_time = None
92+
if await _is_empty_table(pg, models.SlackChannels.c.id):
93+
next_run_time = datetime.datetime.now() + datetime.timedelta(minutes=1)
94+
95+
scheduler.add_job(
96+
tasks.sync_slack_channels,
97+
"cron",
98+
minute=15,
99+
args=(slack_client_, pg),
100+
next_run_time=next_run_time,
101+
)
102+
103+
yield
104+
105+
106+
async def _is_empty_table(pg: asyncpg.pool.Pool, column: sa.Column) -> bool:
107+
async with pg.acquire() as conn:
108+
result = await conn.fetchval(select([column]).limit(1))
109+
return result is None

Diff for: pyslackersweb/models.py

+31
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,35 @@ class Source(Enum):
2929
nullable=False,
3030
),
3131
sa.Index("ix_domain_blocked", "domain", "blocked"),
32+
sa.Index("ix_domains_domain", "domain"),
33+
)
34+
35+
SlackChannels = sa.Table(
36+
"slack_channels",
37+
metadata,
38+
sa.Column("id", sa.Text, primary_key=True),
39+
sa.Column("name", sa.Text, unique=True),
40+
sa.Column("created", sa.DateTime(timezone=True)),
41+
sa.Column("archived", sa.Boolean),
42+
sa.Column("members", sa.Integer),
43+
sa.Column("topic", sa.Text),
44+
sa.Column("purpose", sa.Text),
45+
sa.Index("ix_slack_channels_id", "id"),
46+
sa.Index("ix_slack_channels_name", "name"),
47+
)
48+
49+
SlackUsers = sa.Table(
50+
"slack_users",
51+
metadata,
52+
sa.Column("id", sa.Text, primary_key=True),
53+
sa.Column("name", sa.Text, unique=True),
54+
sa.Column("deleted", sa.Boolean),
55+
sa.Column("admin", sa.Boolean),
56+
sa.Column("bot", sa.Boolean),
57+
sa.Column("timezone", sa.Text),
58+
sa.Column("first_seen", sa.DateTime(timezone=True), default=datetime.now),
59+
sa.Index("ix_slack_users_id", "id"),
60+
sa.Index("ix_slack_users_name", "id", "name"),
61+
sa.Index("ix_slack_users_admin", "id", "admin"),
62+
sa.Index("ix_slack_users_timezone", "id", "timezone"),
3263
)

Diff for: pyslackersweb/sirbot/__init__.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import logging
2+
13
from aiohttp import web
24

3-
from .views import routes
5+
from pyslackersweb.sirbot.views import readthedocs, slack
6+
7+
logger = logging.getLogger(__name__)
48

59

610
async def app_factory() -> web.Application:
@@ -11,5 +15,6 @@ async def app_factory() -> web.Application:
1115
scheduler=None, # populated via parent app signal
1216
)
1317

14-
sirbot.add_routes(routes)
18+
readthedocs.add_routes(sirbot.router)
19+
slack.add_routes(sirbot.router)
1520
return sirbot

Diff for: pyslackersweb/sirbot/database.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import logging
2+
3+
import asyncpg
4+
import sqlalchemy as sa
5+
6+
from pyslackersweb import models
7+
from pyslackersweb.util.log import ContextAwareLoggerAdapter
8+
9+
10+
logger = ContextAwareLoggerAdapter(logging.getLogger(__name__))
11+
12+
13+
async def is_admin(conn: asyncpg.connection.Connection, user: str) -> bool:
14+
return await conn.fetchval(sa.select([models.SlackUsers.c.admin]).where(id=user))

Diff for: pyslackersweb/sirbot/models.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import datetime
2+
import dataclasses
3+
4+
from typing import Optional
5+
from decimal import Decimal
6+
7+
import sqlalchemy as sa
8+
9+
from sqlalchemy.dialects.postgresql import JSONB
10+
11+
from pyslackersweb.models import metadata
12+
13+
14+
@dataclasses.dataclass(frozen=True)
15+
class StockQuote:
16+
# pylint: disable=too-many-instance-attributes
17+
18+
symbol: str
19+
company: str
20+
price: Decimal
21+
change: Decimal
22+
change_percent: Decimal
23+
market_open: Decimal
24+
market_close: Decimal
25+
high: Decimal
26+
low: Decimal
27+
volume: Decimal
28+
time: datetime.datetime
29+
logo: Optional[str] = None
30+
31+
32+
SlackMessage = sa.Table(
33+
"slack_messages",
34+
metadata,
35+
sa.Column("id", sa.Text, primary_key=True),
36+
sa.Column("send_at", sa.DateTime),
37+
sa.Column("user", sa.Text),
38+
sa.Column("channel", sa.Text),
39+
sa.Column("message", sa.Text),
40+
sa.Column("raw", JSONB),
41+
sa.Index("ix_slack_messages_user", "user", "send_at"),
42+
sa.Index("ix_slack_messages_channel", "channel", "send_at"),
43+
sa.Index("ix_slack_messages_user_channel", "user", "channel", "send_at"),
44+
)

Diff for: pyslackersweb/sirbot/settings.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import os
22

3-
# production
3+
IS_PRODUCTION = os.environ.get("PLATFORM_BRANCH") == "master"
4+
5+
# production settings
46
READTHEDOCS_NOTIFICATION_CHANNEL = "community_projects"
7+
SLACK_TEAM_ID = os.environ.get("SLACK_TEAM_ID")
8+
SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "")
9+
SLACK_ADMIN_CHANNEL = os.environ.get("SLACK_ADMIN_CHANNEL", "")
10+
SLACK_INTRODUCTION_CHANNEL = "introductions"
511

6-
# Development
7-
if os.environ.get("PLATFORM_BRANCH") != "master":
12+
# Development settings
13+
if not IS_PRODUCTION:
814
READTHEDOCS_NOTIFICATION_CHANNEL = "general"
15+
SLACK_ADMIN_CHANNEL = "CJ1BWMBDX" # general
16+
SLACK_INTRODUCTION_CHANNEL = "general"

0 commit comments

Comments
 (0)