mirror of
https://github.com/RichieCahill/dotfiles.git
synced 2026-04-17 04:58:19 -04:00
move signal bot to its own DB
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
"""seprating signal_bot database.
|
||||||
|
|
||||||
|
Revision ID: 6b275323f435
|
||||||
|
Revises: 2ef7ba690159
|
||||||
|
Create Date: 2026-03-18 08:34:28.785885
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from python.orm import RichieBase
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "6b275323f435"
|
||||||
|
down_revision: str | None = "2ef7ba690159"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
schema = RichieBase.schema_name
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("device_role", schema=schema)
|
||||||
|
op.drop_table("signal_device", schema=schema)
|
||||||
|
op.drop_table("role", schema=schema)
|
||||||
|
op.drop_table("dead_letter_message", schema=schema)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"dead_letter_message",
|
||||||
|
sa.Column("source", sa.VARCHAR(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column("message", sa.TEXT(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column("received_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"status",
|
||||||
|
postgresql.ENUM("UNPROCESSED", "PROCESSED", name="message_status", schema=schema),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"updated",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_dead_letter_message")),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"role",
|
||||||
|
sa.Column("name", sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"id",
|
||||||
|
sa.SMALLINT(),
|
||||||
|
server_default=sa.text("nextval(f'{schema}.role_id_seq'::regclass)"),
|
||||||
|
autoincrement=True,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"created",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"updated",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_role")),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"name", name=op.f("uq_role_name"), postgresql_include=[], postgresql_nulls_not_distinct=False
|
||||||
|
),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"signal_device",
|
||||||
|
sa.Column("phone_number", sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
||||||
|
sa.Column("safety_number", sa.VARCHAR(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"trust_level",
|
||||||
|
postgresql.ENUM("VERIFIED", "UNVERIFIED", "BLOCKED", name="trust_level", schema=schema),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("last_seen", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
|
||||||
|
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"updated",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_signal_device")),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"phone_number",
|
||||||
|
name=op.f("uq_signal_device_phone_number"),
|
||||||
|
postgresql_include=[],
|
||||||
|
postgresql_nulls_not_distinct=False,
|
||||||
|
),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"device_role",
|
||||||
|
sa.Column("device_id", sa.INTEGER(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column("role_id", sa.SMALLINT(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"updated",
|
||||||
|
postgresql.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
autoincrement=False,
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["device_id"], [f"{schema}.signal_device.id"], name=op.f("fk_device_role_device_id_signal_device")
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["role_id"], [f"{schema}.role.id"], name=op.f("fk_device_role_role_id_role")),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_device_role")),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"device_id",
|
||||||
|
"role_id",
|
||||||
|
name=op.f("uq_device_role_device_role"),
|
||||||
|
postgresql_include=[],
|
||||||
|
postgresql_nulls_not_distinct=False,
|
||||||
|
),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""seprating signal_bot database.
|
||||||
|
|
||||||
|
Revision ID: 6eaf696e07a5
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-03-17 21:35:37.612672
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from python.orm import SignalBotBase
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "6eaf696e07a5"
|
||||||
|
down_revision: str | None = None
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
schema = SignalBotBase.schema_name
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"dead_letter_message",
|
||||||
|
sa.Column("source", sa.String(), nullable=False),
|
||||||
|
sa.Column("message", sa.Text(), nullable=False),
|
||||||
|
sa.Column("received_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"status", postgresql.ENUM("UNPROCESSED", "PROCESSED", name="message_status", schema=schema), nullable=False
|
||||||
|
),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_dead_letter_message")),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"role",
|
||||||
|
sa.Column("name", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column("id", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_role")),
|
||||||
|
sa.UniqueConstraint("name", name=op.f("uq_role_name")),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"signal_device",
|
||||||
|
sa.Column("phone_number", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column("safety_number", sa.String(), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"trust_level",
|
||||||
|
postgresql.ENUM("VERIFIED", "UNVERIFIED", "BLOCKED", name="trust_level", schema=schema),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("last_seen", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_signal_device")),
|
||||||
|
sa.UniqueConstraint("phone_number", name=op.f("uq_signal_device_phone_number")),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"device_role",
|
||||||
|
sa.Column("device_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("role_id", sa.SmallInteger(), nullable=False),
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["device_id"], [f"{schema}.signal_device.id"], name=op.f("fk_device_role_device_id_signal_device")
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["role_id"], [f"{schema}.role.id"], name=op.f("fk_device_role_role_id_role")),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_device_role")),
|
||||||
|
sa.UniqueConstraint("device_id", "role_id", name="uq_device_role_device_role"),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("device_role", schema=schema)
|
||||||
|
op.drop_table("signal_device", schema=schema)
|
||||||
|
op.drop_table("role", schema=schema)
|
||||||
|
op.drop_table("dead_letter_message", schema=schema)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -83,6 +83,13 @@ DATABASES: dict[str, DatabaseConfig] = {
|
|||||||
base_class_name="VanInventoryBase",
|
base_class_name="VanInventoryBase",
|
||||||
models_module="python.orm.van_inventory.models",
|
models_module="python.orm.van_inventory.models",
|
||||||
),
|
),
|
||||||
|
"signal_bot": DatabaseConfig(
|
||||||
|
env_prefix="SIGNALBOT",
|
||||||
|
version_location="python/alembic/signal_bot/versions",
|
||||||
|
base_module="python.orm.signal_bot.base",
|
||||||
|
base_class_name="SignalBotBase",
|
||||||
|
models_module="python.orm.signal_bot.models",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""ORM package exports."""
|
"""ORM package exports."""
|
||||||
|
|
||||||
from python.orm.richie.base import RichieBase
|
from python.orm.richie.base import RichieBase
|
||||||
|
from python.orm.signal_bot.base import SignalBotBase
|
||||||
from python.orm.van_inventory.base import VanInventoryBase
|
from python.orm.van_inventory.base import VanInventoryBase
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"RichieBase",
|
"RichieBase",
|
||||||
|
"SignalBotBase",
|
||||||
"VanInventoryBase",
|
"VanInventoryBase",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,22 +11,15 @@ from python.orm.richie.contact import (
|
|||||||
Need,
|
Need,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
)
|
)
|
||||||
from python.orm.richie.dead_letter_message import DeadLetterMessage
|
|
||||||
from python.orm.richie.signal_device import DeviceRole, RoleRecord, SignalDevice
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Bill",
|
"Bill",
|
||||||
"Contact",
|
"Contact",
|
||||||
"ContactNeed",
|
"ContactNeed",
|
||||||
"ContactRelationship",
|
"ContactRelationship",
|
||||||
"DeadLetterMessage",
|
|
||||||
"DeviceRole",
|
|
||||||
"RoleRecord",
|
|
||||||
"Legislator",
|
"Legislator",
|
||||||
"Need",
|
"Need",
|
||||||
"RelationshipType",
|
"RelationshipType",
|
||||||
"RichieBase",
|
"RichieBase",
|
||||||
"SignalDevice",
|
|
||||||
"TableBase",
|
"TableBase",
|
||||||
"TableBaseBig",
|
"TableBaseBig",
|
||||||
"TableBaseSmall",
|
"TableBaseSmall",
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Dead letter queue for Signal bot messages that fail processing."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import DateTime, Text
|
|
||||||
from sqlalchemy.dialects.postgresql import ENUM
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
from python.orm.richie.base import TableBase
|
|
||||||
from python.signal_bot.models import MessageStatus
|
|
||||||
|
|
||||||
|
|
||||||
class DeadLetterMessage(TableBase):
|
|
||||||
"""A Signal message that failed processing and was sent to the dead letter queue."""
|
|
||||||
|
|
||||||
__tablename__ = "dead_letter_message"
|
|
||||||
|
|
||||||
source: Mapped[str]
|
|
||||||
message: Mapped[str] = mapped_column(Text)
|
|
||||||
received_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
||||||
status: Mapped[MessageStatus] = mapped_column(
|
|
||||||
ENUM(MessageStatus, name="message_status", create_type=True, schema="main"),
|
|
||||||
default=MessageStatus.UNPROCESSED,
|
|
||||||
)
|
|
||||||
16
python/orm/signal_bot/__init__.py
Normal file
16
python/orm/signal_bot/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""Signal bot database ORM exports."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from python.orm.signal_bot.base import SignalBotBase, SignalBotTableBase, SignalBotTableBaseSmall
|
||||||
|
from python.orm.signal_bot.models import DeadLetterMessage, DeviceRole, RoleRecord, SignalDevice
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DeadLetterMessage",
|
||||||
|
"DeviceRole",
|
||||||
|
"RoleRecord",
|
||||||
|
"SignalBotBase",
|
||||||
|
"SignalBotTableBase",
|
||||||
|
"SignalBotTableBaseSmall",
|
||||||
|
"SignalDevice",
|
||||||
|
]
|
||||||
52
python/orm/signal_bot/base.py
Normal file
52
python/orm/signal_bot/base.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Signal bot database ORM base."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, MetaData, SmallInteger, func
|
||||||
|
from sqlalchemy.ext.declarative import AbstractConcreteBase
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
|
from python.orm.common import NAMING_CONVENTION
|
||||||
|
|
||||||
|
|
||||||
|
class SignalBotBase(DeclarativeBase):
|
||||||
|
"""Base class for signal_bot database ORM models."""
|
||||||
|
|
||||||
|
schema_name = "main"
|
||||||
|
|
||||||
|
metadata = MetaData(
|
||||||
|
schema=schema_name,
|
||||||
|
naming_convention=NAMING_CONVENTION,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _TableMixin:
|
||||||
|
"""Shared timestamp columns for all table bases."""
|
||||||
|
|
||||||
|
created: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
)
|
||||||
|
updated: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=func.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalBotTableBaseSmall(_TableMixin, AbstractConcreteBase, SignalBotBase):
|
||||||
|
"""Table with SmallInteger primary key."""
|
||||||
|
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(SmallInteger, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalBotTableBase(_TableMixin, AbstractConcreteBase, SignalBotBase):
|
||||||
|
"""Table with Integer primary key."""
|
||||||
|
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
"""Signal bot device and role ORM models."""
|
"""Signal bot device, role, and dead letter ORM models."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, ForeignKey, SmallInteger, String, UniqueConstraint
|
from sqlalchemy import DateTime, ForeignKey, SmallInteger, String, Text, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import ENUM
|
from sqlalchemy.dialects.postgresql import ENUM
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from python.orm.richie.base import TableBase, TableBaseSmall
|
from python.orm.signal_bot.base import SignalBotTableBase, SignalBotTableBaseSmall
|
||||||
from python.signal_bot.models import TrustLevel
|
from python.signal_bot.models import MessageStatus, TrustLevel
|
||||||
|
|
||||||
|
|
||||||
class RoleRecord(TableBaseSmall):
|
class RoleRecord(SignalBotTableBaseSmall):
|
||||||
"""Lookup table for RBAC roles, keyed by smallint."""
|
"""Lookup table for RBAC roles, keyed by smallint."""
|
||||||
|
|
||||||
__tablename__ = "role"
|
__tablename__ = "role"
|
||||||
@@ -20,7 +20,7 @@ class RoleRecord(TableBaseSmall):
|
|||||||
name: Mapped[str] = mapped_column(String(50), unique=True)
|
name: Mapped[str] = mapped_column(String(50), unique=True)
|
||||||
|
|
||||||
|
|
||||||
class DeviceRole(TableBase):
|
class DeviceRole(SignalBotTableBase):
|
||||||
"""Association between a device and a role."""
|
"""Association between a device and a role."""
|
||||||
|
|
||||||
__tablename__ = "device_role"
|
__tablename__ = "device_role"
|
||||||
@@ -33,7 +33,7 @@ class DeviceRole(TableBase):
|
|||||||
role_id: Mapped[int] = mapped_column(SmallInteger, ForeignKey("main.role.id"))
|
role_id: Mapped[int] = mapped_column(SmallInteger, ForeignKey("main.role.id"))
|
||||||
|
|
||||||
|
|
||||||
class SignalDevice(TableBase):
|
class SignalDevice(SignalBotTableBase):
|
||||||
"""A Signal device tracked by phone number and safety number."""
|
"""A Signal device tracked by phone number and safety number."""
|
||||||
|
|
||||||
__tablename__ = "signal_device"
|
__tablename__ = "signal_device"
|
||||||
@@ -47,3 +47,17 @@ class SignalDevice(TableBase):
|
|||||||
last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
roles: Mapped[list[RoleRecord]] = relationship(secondary=DeviceRole.__table__)
|
roles: Mapped[list[RoleRecord]] = relationship(secondary=DeviceRole.__table__)
|
||||||
|
|
||||||
|
|
||||||
|
class DeadLetterMessage(SignalBotTableBase):
|
||||||
|
"""A Signal message that failed processing and was sent to the dead letter queue."""
|
||||||
|
|
||||||
|
__tablename__ = "dead_letter_message"
|
||||||
|
|
||||||
|
source: Mapped[str]
|
||||||
|
message: Mapped[str] = mapped_column(Text)
|
||||||
|
received_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
status: Mapped[MessageStatus] = mapped_column(
|
||||||
|
ENUM(MessageStatus, name="message_status", create_type=True, schema="main"),
|
||||||
|
default=MessageStatus.UNPROCESSED,
|
||||||
|
)
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -10,11 +11,15 @@ if TYPE_CHECKING:
|
|||||||
from python.signal_bot.models import SignalMessage
|
from python.signal_bot.models import SignalMessage
|
||||||
from python.signal_bot.signal_client import SignalClient
|
from python.signal_bot.signal_client import SignalClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_entity_state(ha_url: str, ha_token: str, entity_id: str) -> dict[str, Any]:
|
def _get_entity_state(ha_url: str, ha_token: str, entity_id: str) -> dict[str, Any]:
|
||||||
"""Fetch an entity's state from Home Assistant."""
|
"""Fetch an entity's state from Home Assistant."""
|
||||||
|
entity_url = f"{ha_url}/api/states/{entity_id}"
|
||||||
|
logger.debug(f"Fetching {entity_url=}")
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{ha_url}/api/states/{entity_id}",
|
entity_url,
|
||||||
headers={"Authorization": f"Bearer {ha_token}"},
|
headers={"Authorization": f"Bearer {ha_token}"},
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
@@ -24,10 +29,7 @@ def _get_entity_state(ha_url: str, ha_token: str, entity_id: str) -> dict[str, A
|
|||||||
|
|
||||||
def _format_location(latitude: str, longitude: str) -> str:
|
def _format_location(latitude: str, longitude: str) -> str:
|
||||||
"""Render a friendly location response."""
|
"""Render a friendly location response."""
|
||||||
return (
|
return f"Van location: {latitude}, {longitude}\nhttps://maps.google.com/?q={latitude},{longitude}"
|
||||||
f"Van location: {latitude}, {longitude}\n"
|
|
||||||
f"https://maps.google.com/?q={latitude},{longitude}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_location_request(
|
def handle_location_request(
|
||||||
@@ -41,10 +43,14 @@ def handle_location_request(
|
|||||||
signal.reply(message, "Location command is not configured (missing HA_URL or HA_TOKEN).")
|
signal.reply(message, "Location command is not configured (missing HA_URL or HA_TOKEN).")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
lat_payload = None
|
||||||
|
lon_payload = None
|
||||||
try:
|
try:
|
||||||
lat_payload = _get_entity_state(ha_url, ha_token, "sensor.van_last_known_latitude")
|
lat_payload = _get_entity_state(ha_url, ha_token, "sensor.van_last_known_latitude")
|
||||||
lon_payload = _get_entity_state(ha_url, ha_token, "sensor.van_last_known_longitude")
|
lon_payload = _get_entity_state(ha_url, ha_token, "sensor.van_last_known_longitude")
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
|
logger.exception("Couldn't fetch van location from Home Assistant right now.")
|
||||||
|
logger.debug(f"{ha_url=} {lat_payload=} {lon_payload=}")
|
||||||
signal.reply(message, "Couldn't fetch van location from Home Assistant right now.")
|
signal.reply(message, "Couldn't fetch van location from Home Assistant right now.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from sqlalchemy import delete, select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from python.common import utcnow
|
from python.common import utcnow
|
||||||
from python.orm.richie.signal_device import RoleRecord, SignalDevice
|
from python.orm.signal_bot.models import RoleRecord, SignalDevice
|
||||||
from python.signal_bot.models import Role, TrustLevel
|
from python.signal_bot.models import Role, TrustLevel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ class LLMClient:
|
|||||||
temperature: Sampling temperature.
|
temperature: Sampling temperature.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, model: str, host: str, port: int = 11434, *, temperature: float = 0.1) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
model: str,
|
||||||
|
host: str,
|
||||||
|
port: int = 11434,
|
||||||
|
temperature: float = 0.1,
|
||||||
|
timeout: int = 300,
|
||||||
|
) -> None:
|
||||||
self.model = model
|
self.model = model
|
||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
self._client = httpx.Client(base_url=f"http://{host}:{port}", timeout=120)
|
self._client = httpx.Client(base_url=f"http://{host}:{port}", timeout=timeout)
|
||||||
|
|
||||||
def chat(self, prompt: str, image_data: bytes | None = None, system: str | None = None) -> str:
|
def chat(self, prompt: str, image_data: bytes | None = None, system: str | None = None) -> str:
|
||||||
"""Send a text prompt and return the response."""
|
"""Send a text prompt and return the response."""
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ if TYPE_CHECKING:
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from alembic.command import upgrade
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from tenacity import before_sleep_log, retry, stop_after_attempt, wait_exponential
|
from tenacity import before_sleep_log, retry, stop_after_attempt, wait_exponential
|
||||||
|
|
||||||
from python.common import configure_logger, utcnow
|
from python.common import configure_logger, utcnow
|
||||||
|
from python.database_cli import DATABASES
|
||||||
from python.orm.common import get_postgres_engine
|
from python.orm.common import get_postgres_engine
|
||||||
from python.orm.richie.dead_letter_message import DeadLetterMessage
|
from python.orm.signal_bot.models import DeadLetterMessage
|
||||||
from python.signal_bot.commands.inventory import handle_inventory_update
|
from python.signal_bot.commands.inventory import handle_inventory_update
|
||||||
from python.signal_bot.commands.location import handle_location_request
|
from python.signal_bot.commands.location import handle_location_request
|
||||||
from python.signal_bot.device_registry import DeviceRegistry, sync_roles
|
from python.signal_bot.device_registry import DeviceRegistry, sync_roles
|
||||||
@@ -181,7 +183,7 @@ class Bot:
|
|||||||
|
|
||||||
|
|
||||||
def main(
|
def main(
|
||||||
log_level: Annotated[str, typer.Option()] = "INFO",
|
log_level: Annotated[str, typer.Option()] = "DEBUG",
|
||||||
llm_timeout: Annotated[int, typer.Option()] = 600,
|
llm_timeout: Annotated[int, typer.Option()] = 600,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run the Signal command and control bot."""
|
"""Run the Signal command and control bot."""
|
||||||
@@ -200,6 +202,8 @@ def main(
|
|||||||
error = "INVENTORY_API_URL environment variable not set"
|
error = "INVENTORY_API_URL environment variable not set"
|
||||||
raise ValueError(error)
|
raise ValueError(error)
|
||||||
|
|
||||||
|
signal_bot_config = DATABASES["signal_bot"].alembic_config()
|
||||||
|
upgrade(signal_bot_config, "head")
|
||||||
engine = get_postgres_engine(name="SIGNALBOT")
|
engine = get_postgres_engine(name="SIGNALBOT")
|
||||||
sync_roles(engine)
|
sync_roles(engine)
|
||||||
config = BotConfig(
|
config = BotConfig(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ in
|
|||||||
local gitea gitea trust
|
local gitea gitea trust
|
||||||
|
|
||||||
# signalbot
|
# signalbot
|
||||||
local richie signalbot trust
|
local signalbot signalbot trust
|
||||||
|
|
||||||
# math
|
# math
|
||||||
local postgres math trust
|
local postgres math trust
|
||||||
@@ -103,6 +103,7 @@ in
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "signalbot";
|
name = "signalbot";
|
||||||
|
ensureDBOwnership = true;
|
||||||
ensureClauses = {
|
ensureClauses = {
|
||||||
login = true;
|
login = true;
|
||||||
};
|
};
|
||||||
@@ -114,6 +115,7 @@ in
|
|||||||
"math"
|
"math"
|
||||||
"n8n"
|
"n8n"
|
||||||
"richie"
|
"richie"
|
||||||
|
"signalbot"
|
||||||
];
|
];
|
||||||
# Thank you NotAShelf
|
# Thank you NotAShelf
|
||||||
# https://github.com/NotAShelf/nyx/blob/d407b4d6e5ab7f60350af61a3d73a62a5e9ac660/modules/core/roles/server/system/services/databases/postgresql.nix#L74
|
# https://github.com/NotAShelf/nyx/blob/d407b4d6e5ab7f60350af61a3d73a62a5e9ac660/modules/core/roles/server/system/services/databases/postgresql.nix#L74
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ in
|
|||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
PYTHONPATH = "${inputs.self}";
|
PYTHONPATH = "${inputs.self}";
|
||||||
SIGNALBOT_DB = "richie";
|
SIGNALBOT_DB = "signalbot";
|
||||||
SIGNALBOT_USER = "signalbot";
|
SIGNALBOT_USER = "signalbot";
|
||||||
SIGNALBOT_HOST = "/run/postgresql";
|
SIGNALBOT_HOST = "/run/postgresql";
|
||||||
SIGNALBOT_PORT = "5432";
|
SIGNALBOT_PORT = "5432";
|
||||||
@@ -34,6 +34,7 @@ in
|
|||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
|
WorkingDirectory = "${inputs.self}";
|
||||||
User = "signalbot";
|
User = "signalbot";
|
||||||
Group = "signalbot";
|
Group = "signalbot";
|
||||||
EnvironmentFile = "${vars.secrets}/services/signal-bot";
|
EnvironmentFile = "${vars.secrets}/services/signal-bot";
|
||||||
|
|||||||
Reference in New Issue
Block a user