Browse Source

Add real-time bridge status push option

Tulir Asokan 4 years ago
parent
commit
bfcd7bb43d
4 changed files with 34 additions and 9 deletions
  1. 4 0
      mautrix_signal/example-config.yaml
  2. 27 6
      mautrix_signal/user.py
  3. 1 1
      optional-requirements.txt
  4. 2 2
      requirements.txt

+ 4 - 0
mautrix_signal/example-config.yaml

@@ -10,6 +10,10 @@ homeserver:
     asmux: false
     asmux: false
     # Number of retries for all HTTP requests if the homeserver isn't reachable.
     # Number of retries for all HTTP requests if the homeserver isn't reachable.
     http_retry_count: 4
     http_retry_count: 4
+    # The URL to push real-time bridge status to.
+    # If set, the bridge will make POST requests to this URL whenever a user's Signal connection state changes.
+    # The bridge will use the appservice as_token to authorize requests.
+    status_endpoint: null
 
 
 # Application service host/registration related details
 # Application service host/registration related details
 # Changing these values requires regeneration of the registration.
 # Changing these values requires regeneration of the registration.

+ 27 - 6
mautrix_signal/user.py

@@ -14,12 +14,11 @@
 # You should have received a copy of the GNU Affero General Public License
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 from typing import Union, Dict, Optional, AsyncGenerator, TYPE_CHECKING, cast
 from typing import Union, Dict, Optional, AsyncGenerator, TYPE_CHECKING, cast
-from collections import defaultdict
 from uuid import UUID
 from uuid import UUID
 import asyncio
 import asyncio
 
 
 from mausignald.types import Account, Address, Profile, Group, GroupV2, ListenEvent, ListenAction
 from mausignald.types import Account, Address, Profile, Group, GroupV2, ListenEvent, ListenAction
-from mautrix.bridge import BaseUser, async_getter_lock
+from mautrix.bridge import BaseUser, BridgeState, async_getter_lock
 from mautrix.types import UserID, RoomID
 from mautrix.types import UserID, RoomID
 from mautrix.appservice import AppService
 from mautrix.appservice import AppService
 from mautrix.util.opt_prometheus import Gauge
 from mautrix.util.opt_prometheus import Gauge
@@ -34,6 +33,11 @@ if TYPE_CHECKING:
 METRIC_CONNECTED = Gauge('bridge_connected', 'Bridge users connected to Signal')
 METRIC_CONNECTED = Gauge('bridge_connected', 'Bridge users connected to Signal')
 METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Bridge users logged into Signal')
 METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Bridge users logged into Signal')
 
 
+BridgeState.human_readable_errors.update({
+    "logged-out": "You're not logged into Signal",
+    "signal-not-connected": None,
+})
+
 
 
 class User(DBUser, BaseUser):
 class User(DBUser, BaseUser):
     by_mxid: Dict[UserID, 'User'] = {}
     by_mxid: Dict[UserID, 'User'] = {}
@@ -49,18 +53,17 @@ class User(DBUser, BaseUser):
 
 
     _sync_lock: asyncio.Lock
     _sync_lock: asyncio.Lock
     _notice_room_lock: asyncio.Lock
     _notice_room_lock: asyncio.Lock
+    _connected: bool
 
 
     def __init__(self, mxid: UserID, username: Optional[str] = None, uuid: Optional[UUID] = None,
     def __init__(self, mxid: UserID, username: Optional[str] = None, uuid: Optional[UUID] = None,
                  notice_room: Optional[RoomID] = None) -> None:
                  notice_room: Optional[RoomID] = None) -> None:
         super().__init__(mxid=mxid, username=username, uuid=uuid, notice_room=notice_room)
         super().__init__(mxid=mxid, username=username, uuid=uuid, notice_room=notice_room)
+        BaseUser.__init__(self)
         self._notice_room_lock = asyncio.Lock()
         self._notice_room_lock = asyncio.Lock()
         self._sync_lock = asyncio.Lock()
         self._sync_lock = asyncio.Lock()
+        self._connected = False
         perms = self.config.get_permissions(mxid)
         perms = self.config.get_permissions(mxid)
         self.is_whitelisted, self.is_admin, self.permission_level = perms
         self.is_whitelisted, self.is_admin, self.permission_level = perms
-        self.log = self.log.getChild(self.mxid)
-        self.dm_update_lock = asyncio.Lock()
-        self.command_status = None
-        self._metric_value = defaultdict(lambda: False)
 
 
     @classmethod
     @classmethod
     def init_cls(cls, bridge: 'SignalBridge') -> None:
     def init_cls(cls, bridge: 'SignalBridge') -> None:
@@ -94,6 +97,20 @@ class User(DBUser, BaseUser):
         await asyncio.sleep(1)
         await asyncio.sleep(1)
         await self.bridge.signal.delete_account(username)
         await self.bridge.signal.delete_account(username)
         self._track_metric(METRIC_LOGGED_IN, False)
         self._track_metric(METRIC_LOGGED_IN, False)
+        await self.push_bridge_state(ok=False, error="logged-out")
+
+    async def fill_bridge_state(self, state: BridgeState) -> None:
+        await super().fill_bridge_state(state)
+        state.remote_id = self.username
+        puppet = await pu.Puppet.get_by_address(self.address)
+        state.remote_name = puppet.name or self.username
+
+    async def get_bridge_state(self) -> BridgeState:
+        if not self.username:
+            return BridgeState(ok=False, error="logged-out")
+        elif not self._connected:
+            return BridgeState(ok=False, error="signal-not-connected")
+        return BridgeState(ok=True)
 
 
     async def on_signin(self, account: Account) -> None:
     async def on_signin(self, account: Account) -> None:
         self.username = account.account_id
         self.username = account.account_id
@@ -109,12 +126,16 @@ class User(DBUser, BaseUser):
             self.log.info("Connected to Signal")
             self.log.info("Connected to Signal")
             self._track_metric(METRIC_CONNECTED, True)
             self._track_metric(METRIC_CONNECTED, True)
             self._track_metric(METRIC_LOGGED_IN, True)
             self._track_metric(METRIC_LOGGED_IN, True)
+            self._connected = True
+            asyncio.create_task(self.push_bridge_state(ok=True))
         elif evt.action == ListenAction.STOPPED:
         elif evt.action == ListenAction.STOPPED:
             if evt.exception:
             if evt.exception:
                 self.log.warning(f"Disconnected from Signal: {evt.exception}")
                 self.log.warning(f"Disconnected from Signal: {evt.exception}")
             else:
             else:
                 self.log.info("Disconnected from Signal")
                 self.log.info("Disconnected from Signal")
             self._track_metric(METRIC_CONNECTED, False)
             self._track_metric(METRIC_CONNECTED, False)
+            asyncio.create_task(self.push_bridge_state(ok=False, error="signal-not-connected"))
+            self._connected = False
         else:
         else:
             self.log.warning(f"Unrecognized listen action {evt.action}")
             self.log.warning(f"Unrecognized listen action {evt.action}")
 
 

+ 1 - 1
optional-requirements.txt

@@ -7,7 +7,7 @@ pycryptodome>=3,<4
 unpaddedbase64>=1,<2
 unpaddedbase64>=1,<2
 
 
 #/metrics
 #/metrics
-prometheus_client>=0.6,<0.11
+prometheus_client>=0.6,<0.12
 
 
 #/formattednumbers
 #/formattednumbers
 phonenumbers>=8,<9
 phonenumbers>=8,<9

+ 2 - 2
requirements.txt

@@ -4,5 +4,5 @@ commonmark>=0.8,<0.10
 aiohttp>=3,<4
 aiohttp>=3,<4
 yarl>=1,<2
 yarl>=1,<2
 attrs>=19.1
 attrs>=19.1
-mautrix>=0.9,<0.10
-asyncpg>=0.20,<0.23
+mautrix>=0.9.5,<0.10
+asyncpg>=0.20,<0.24