diff --git a/mautrix_twitter/__main__.py b/mautrix_twitter/__main__.py
index 419ec94..255003d 100644
--- a/mautrix_twitter/__main__.py
+++ b/mautrix_twitter/__main__.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2021 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Any, Dict
+from __future__ import annotations
+
+from typing import Any
from mautrix.bridge import Bridge
from mautrix.types import RoomID, UserID
@@ -94,7 +96,7 @@ def is_bridge_ghost(self, user_id: UserID) -> bool:
async def count_logged_in_users(self) -> int:
return len([user for user in User.by_twid.values() if user.twid])
- async def manhole_global_namespace(self, user_id: UserID) -> Dict[str, Any]:
+ async def manhole_global_namespace(self, user_id: UserID) -> dict[str, Any]:
return {
**await super().manhole_global_namespace(user_id),
"User": User,
diff --git a/mautrix_twitter/commands/auth.py b/mautrix_twitter/commands/auth.py
index 48b2ac7..e8bea69 100644
--- a/mautrix_twitter/commands/auth.py
+++ b/mautrix_twitter/commands/auth.py
@@ -62,7 +62,7 @@ async def enter_login_cookies(evt: CommandEvent) -> None:
return
if len(evt.args) == 0:
await evt.reply(
- "Please enter the value of the `ct0` cookie, or use " "the `cancel` command to cancel."
+ "Please enter the value of the `ct0` cookie, or use the `cancel` command to cancel."
)
return
@@ -83,7 +83,7 @@ async def enter_login_cookies(evt: CommandEvent) -> None:
@command_handler(
needs_auth=True,
help_section=SECTION_AUTH,
- help_text="Disconnect the bridge from" "your Twitter account",
+ help_text="Disconnect the bridge from your Twitter account",
)
async def logout(evt: CommandEvent) -> None:
await evt.sender.logout()
diff --git a/mautrix_twitter/config.py b/mautrix_twitter/config.py
index 6a02bdb..d357ae6 100644
--- a/mautrix_twitter/config.py
+++ b/mautrix_twitter/config.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Any, List, NamedTuple
+from __future__ import annotations
+
+from typing import Any, NamedTuple
import os
from mautrix.bridge.config import BaseBridgeConfig
@@ -32,7 +34,7 @@ def __getitem__(self, key: str) -> Any:
return super().__getitem__(key)
@property
- def forbidden_defaults(self) -> List[ForbiddenDefault]:
+ def forbidden_defaults(self) -> list[ForbiddenDefault]:
return [
*super().forbidden_defaults,
ForbiddenDefault("appservice.database", "postgres://username:password@hostname/db"),
@@ -51,8 +53,6 @@ def do_update(self, helper: ConfigUpdateHelper) -> None:
if base["appservice.provisioning.shared_secret"] == "generate":
base["appservice.provisioning.shared_secret"] = self._new_token()
- copy("appservice.community_id")
-
copy("metrics.enabled")
copy("metrics.listen_port")
diff --git a/mautrix_twitter/db/message.py b/mautrix_twitter/db/message.py
index 7c9a0f9..f756734 100644
--- a/mautrix_twitter/db/message.py
+++ b/mautrix_twitter/db/message.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,14 +13,16 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, ClassVar, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
from attr import dataclass
from mautrix.types import EventID, RoomID
from mautrix.util.async_db import Database
-fake_db = Database("") if TYPE_CHECKING else None
+fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
@@ -45,23 +47,17 @@ async def delete_all(cls, room_id: RoomID) -> None:
await cls.db.execute("DELETE FROM message WHERE mx_room=$1", room_id)
@classmethod
- async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional["Message"]:
- row = await cls.db.fetchrow(
- "SELECT mxid, mx_room, twid, receiver " "FROM message WHERE mxid=$1 AND mx_room=$2",
- mxid,
- mx_room,
- )
+ async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Message | None:
+ q = "SELECT mxid, mx_room, twid, receiver FROM message WHERE mxid=$1 AND mx_room=$2"
+ row = await cls.db.fetchrow(q, mxid, mx_room)
if not row:
return None
return cls(**row)
@classmethod
- async def get_by_twid(cls, twid: int, receiver: int = 0) -> Optional["Message"]:
- row = await cls.db.fetchrow(
- "SELECT mxid, mx_room, twid, receiver " "FROM message WHERE twid=$1 AND receiver=$2",
- twid,
- receiver,
- )
+ async def get_by_twid(cls, twid: int, receiver: int = 0) -> Message | None:
+ q = "SELECT mxid, mx_room, twid, receiver FROM message WHERE twid=$1 AND receiver=$2"
+ row = await cls.db.fetchrow(q, twid, receiver)
if not row:
return None
return cls(**row)
diff --git a/mautrix_twitter/db/portal.py b/mautrix_twitter/db/portal.py
index 17add30..6a0e34e 100644
--- a/mautrix_twitter/db/portal.py
+++ b/mautrix_twitter/db/portal.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, ClassVar, List, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
from attr import dataclass
import asyncpg
@@ -22,7 +24,7 @@
from mautrix.util.async_db import Database
from mautwitdm.types import ConversationType
-fake_db = Database("") if TYPE_CHECKING else None
+fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
@@ -32,18 +34,14 @@ class Portal:
twid: str
receiver: int
conv_type: ConversationType
- other_user: Optional[int]
- mxid: Optional[RoomID]
- name: Optional[str]
+ other_user: int | None
+ mxid: RoomID | None
+ name: str | None
encrypted: bool
- async def insert(self) -> None:
- q = (
- "INSERT INTO portal (twid, receiver, conv_type, other_user, mxid, name, encrypted) "
- "VALUES ($1, $2, $3, $4, $5, $6, $7)"
- )
- await self.db.execute(
- q,
+ @property
+ def _values(self):
+ return (
self.twid,
self.receiver,
self.conv_type.value,
@@ -53,21 +51,19 @@ async def insert(self) -> None:
self.encrypted,
)
+ async def insert(self) -> None:
+ q = (
+ "INSERT INTO portal (twid, receiver, conv_type, other_user, mxid, name, encrypted) "
+ "VALUES ($1, $2, $3, $4, $5, $6, $7)"
+ )
+ await self.db.execute(q, *self._values)
+
async def update(self) -> None:
q = (
"UPDATE portal SET conv_type=$3, other_user=$4, mxid=$5, name=$6, encrypted=$7 "
"WHERE twid=$1 AND receiver=$2"
)
- await self.db.execute(
- q,
- self.twid,
- self.receiver,
- self.conv_type.value,
- self.other_user,
- self.mxid,
- self.name,
- self.encrypted,
- )
+ await self.db.execute(q, *self._values)
@classmethod
def _from_row(cls, row: asyncpg.Record) -> "Portal":
@@ -75,7 +71,7 @@ def _from_row(cls, row: asyncpg.Record) -> "Portal":
return cls(conv_type=ConversationType(data.pop("conv_type")), **data)
@classmethod
- async def get_by_mxid(cls, mxid: RoomID) -> Optional["Portal"]:
+ async def get_by_mxid(cls, mxid: RoomID) -> Portal | None:
q = (
"SELECT twid, receiver, conv_type, other_user, mxid, name, encrypted "
"FROM portal WHERE mxid=$1"
@@ -86,7 +82,7 @@ async def get_by_mxid(cls, mxid: RoomID) -> Optional["Portal"]:
return cls._from_row(row)
@classmethod
- async def get_by_twid(cls, twid: str, receiver: int = 0) -> Optional["Portal"]:
+ async def get_by_twid(cls, twid: str, receiver: int = 0) -> Portal | None:
q = (
"SELECT twid, receiver, conv_type, other_user, mxid, name, encrypted "
"FROM portal WHERE twid=$1 AND receiver=$2"
@@ -97,7 +93,7 @@ async def get_by_twid(cls, twid: str, receiver: int = 0) -> Optional["Portal"]:
return cls._from_row(row)
@classmethod
- async def find_private_chats_of(cls, receiver: int) -> List["Portal"]:
+ async def find_private_chats_of(cls, receiver: int) -> list[Portal]:
q = (
"SELECT twid, receiver, conv_type, other_user, mxid, name, encrypted FROM portal "
"WHERE receiver=$1 AND conv_type='ONE_TO_ONE'"
@@ -106,7 +102,7 @@ async def find_private_chats_of(cls, receiver: int) -> List["Portal"]:
return [cls._from_row(row) for row in rows]
@classmethod
- async def find_private_chats_with(cls, other_user: int) -> List["Portal"]:
+ async def find_private_chats_with(cls, other_user: int) -> list[Portal]:
q = (
"SELECT twid, receiver, conv_type, other_user, mxid, name, encrypted FROM portal "
"WHERE other_user=$1 AND conv_type='ONE_TO_ONE'"
@@ -115,7 +111,7 @@ async def find_private_chats_with(cls, other_user: int) -> List["Portal"]:
return [cls._from_row(row) for row in rows]
@classmethod
- async def all_with_room(cls) -> List["Portal"]:
+ async def all_with_room(cls) -> list[Portal]:
q = (
"SELECT twid, receiver, conv_type, other_user, mxid, name, encrypted FROM portal "
"WHERE mxid IS NOT NULL"
diff --git a/mautrix_twitter/db/puppet.py b/mautrix_twitter/db/puppet.py
index 8b49f2a..a8e28c7 100644
--- a/mautrix_twitter/db/puppet.py
+++ b/mautrix_twitter/db/puppet.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, ClassVar, List, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
from attr import dataclass
from yarl import URL
@@ -22,7 +24,7 @@
from mautrix.types import ContentURI, SyncToken, UserID
from mautrix.util.async_db import Database
-fake_db = Database("") if TYPE_CHECKING else None
+fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
@@ -30,25 +32,20 @@ class Puppet:
db: ClassVar[Database] = fake_db
twid: int
- name: Optional[str]
- photo_url: Optional[str]
- photo_mxc: Optional[ContentURI]
+ name: str | None
+ photo_url: str | None
+ photo_mxc: ContentURI | None
is_registered: bool
- custom_mxid: Optional[UserID]
- access_token: Optional[str]
- next_batch: Optional[SyncToken]
- base_url: Optional[URL]
+ custom_mxid: UserID | None
+ access_token: str | None
+ next_batch: SyncToken | None
+ base_url: URL | None
- async def insert(self) -> None:
- q = (
- "INSERT INTO puppet (twid, name, photo_url, photo_mxc, is_registered, custom_mxid,"
- " access_token, next_batch, base_url) "
- "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
- )
- await self.db.execute(
- q,
+ @property
+ def _values(self):
+ return (
self.twid,
self.name,
self.photo_url,
@@ -60,61 +57,54 @@ async def insert(self) -> None:
str(self.base_url) if self.base_url else None,
)
+ async def insert(self) -> None:
+ q = (
+ "INSERT INTO puppet (twid, name, photo_url, photo_mxc, is_registered, custom_mxid,"
+ " access_token, next_batch, base_url) "
+ "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
+ )
+ await self.db.execute(q, *self._values)
+
async def update(self) -> None:
q = (
"UPDATE puppet SET name=$2, photo_url=$3, photo_mxc=$4, is_registered=$5,"
" custom_mxid=$6, access_token=$7, next_batch=$8, base_url=$9 "
"WHERE twid=$1"
)
- await self.db.execute(
- q,
- self.twid,
- self.name,
- self.photo_url,
- self.photo_mxc,
- self.is_registered,
- self.custom_mxid,
- self.access_token,
- self.next_batch,
- str(self.base_url) if self.base_url else None,
- )
+ await self.db.execute(q, *self._values)
@classmethod
- def _from_row(cls, row: asyncpg.Record) -> "Puppet":
+ def _from_row(cls, row: asyncpg.Record) -> Puppet | None:
+ if not row:
+ return None
data = {**row}
base_url_str = data.pop("base_url")
base_url = URL(base_url_str) if base_url_str is not None else None
return cls(base_url=base_url, **data)
@classmethod
- async def get_by_twid(cls, twid: int) -> Optional["Puppet"]:
- row = await cls.db.fetchrow(
+ async def get_by_twid(cls, twid: int) -> Puppet | None:
+ q = (
"SELECT twid, name, photo_url, photo_mxc, is_registered,"
" custom_mxid, access_token, next_batch, base_url "
- "FROM puppet WHERE twid=$1",
- twid,
+ "FROM puppet WHERE twid=$1"
)
- if not row:
- return None
- return cls._from_row(row)
+ return cls._from_row(await cls.db.fetchrow(q, twid))
@classmethod
- async def get_by_custom_mxid(cls, mxid: UserID) -> Optional["Puppet"]:
- row = await cls.db.fetchrow(
+ async def get_by_custom_mxid(cls, mxid: UserID) -> Puppet | None:
+ q = (
"SELECT twid, name, photo_url, photo_mxc, is_registered,"
" custom_mxid, access_token, next_batch, base_url "
- "FROM puppet WHERE custom_mxid=$1",
- mxid,
+ "FROM puppet WHERE custom_mxid=$1"
)
- if not row:
- return None
- return cls._from_row(row)
+ return cls._from_row(await cls.db.fetchrow(q, mxid))
@classmethod
- async def all_with_custom_mxid(cls) -> List["Puppet"]:
- rows = await cls.db.fetch(
+ async def all_with_custom_mxid(cls) -> list[Puppet]:
+ q = (
"SELECT twid, name, photo_url, photo_mxc, is_registered,"
" custom_mxid, access_token, next_batch, base_url "
"FROM puppet WHERE custom_mxid IS NOT NULL"
)
- return [cls._from_row(row) for row in rows]
+ return [cls._from_row(row) for row in await cls.db.fetch(q)]
diff --git a/mautrix_twitter/db/reaction.py b/mautrix_twitter/db/reaction.py
index 51340ce..afcc0a7 100644
--- a/mautrix_twitter/db/reaction.py
+++ b/mautrix_twitter/db/reaction.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,15 +13,18 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, ClassVar, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
from attr import dataclass
+import asyncpg
from mautrix.types import EventID, RoomID
from mautrix.util.async_db import Database
from mautwitdm.types import ReactionKey
-fake_db = Database("") if TYPE_CHECKING else None
+fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
@@ -51,9 +54,12 @@ async def insert(self) -> None:
)
async def edit(self, mx_room: RoomID, mxid: EventID, reaction: ReactionKey) -> None:
- await self.db.execute(
+ q = (
"UPDATE reaction SET mxid=$1, mx_room=$2, reaction=$3 "
- "WHERE tw_msgid=$4 AND tw_receiver=$5 AND tw_sender=$6",
+ "WHERE tw_msgid=$4 AND tw_receiver=$5 AND tw_sender=$6"
+ )
+ await self.db.execute(
+ q,
mxid,
mx_room,
reaction.value,
@@ -67,16 +73,20 @@ async def delete(self) -> None:
await self.db.execute(q, self.tw_msgid, self.tw_receiver, self.tw_sender)
@classmethod
- async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional["Reaction"]:
+ def _from_row(cls, row: asyncpg.Record) -> Reaction | None:
+ if not row:
+ return None
+ data = {**row}
+ reaction = ReactionKey(data.pop("reaction"))
+ return cls(reaction=reaction, **data)
+
+ @classmethod
+ async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Reaction | None:
q = (
"SELECT mxid, mx_room, tw_msgid, tw_receiver, tw_sender, reaction "
"FROM reaction WHERE mxid=$1 AND mx_room=$2"
)
- row = await cls.db.fetchrow(q, mxid, mx_room)
- if not row:
- return None
- data = {**row}
- return cls(reaction=ReactionKey(data.pop("reaction")), **data)
+ return cls._from_row(await cls.db.fetchrow(q, mxid, mx_room))
@classmethod
async def get_by_twid(
@@ -84,13 +94,9 @@ async def get_by_twid(
tw_msgid: int,
tw_receiver: int,
tw_sender: int,
- ) -> Optional["Reaction"]:
+ ) -> Reaction | None:
q = (
"SELECT mxid, mx_room, tw_msgid, tw_receiver, tw_sender, reaction "
"FROM reaction WHERE tw_msgid=$1 AND tw_sender=$2 AND tw_receiver=$3"
)
- row = await cls.db.fetchrow(q, tw_msgid, tw_sender, tw_receiver)
- if not row:
- return None
- data = {**row}
- return cls(reaction=ReactionKey(data.pop("reaction")), **data)
+ return cls._from_row(await cls.db.fetchrow(q, tw_msgid, tw_sender, tw_receiver))
diff --git a/mautrix_twitter/db/upgrade.py b/mautrix_twitter/db/upgrade.py
index 2d632b4..609db28 100644
--- a/mautrix_twitter/db/upgrade.py
+++ b/mautrix_twitter/db/upgrade.py
@@ -25,82 +25,82 @@ async def upgrade_v1(conn: Connection) -> None:
await conn.execute("CREATE TYPE twitter_conv_type AS ENUM ('ONE_TO_ONE', 'GROUP_DM')")
await conn.execute(
"""CREATE TABLE portal (
- twid VARCHAR(255),
- receiver BIGINT,
- conv_type twitter_conv_type NOT NULL,
- other_user BIGINT,
- mxid VARCHAR(255),
- name VARCHAR(255),
- encrypted BOOLEAN NOT NULL DEFAULT false,
+ twid VARCHAR(255),
+ receiver BIGINT,
+ conv_type twitter_conv_type NOT NULL,
+ other_user BIGINT,
+ mxid VARCHAR(255),
+ name VARCHAR(255),
+ encrypted BOOLEAN NOT NULL DEFAULT false,
- PRIMARY KEY (twid, receiver)
- )"""
+ PRIMARY KEY (twid, receiver)
+ )"""
)
await conn.execute(
"""CREATE TABLE "user" (
- mxid VARCHAR(255) PRIMARY KEY,
- twid BIGINT,
- auth_token VARCHAR(255),
- csrf_token VARCHAR(255),
- poll_cursor VARCHAR(255),
- notice_room VARCHAR(255)
- )"""
+ mxid VARCHAR(255) PRIMARY KEY,
+ twid BIGINT,
+ auth_token VARCHAR(255),
+ csrf_token VARCHAR(255),
+ poll_cursor VARCHAR(255),
+ notice_room VARCHAR(255)
+ )"""
)
await conn.execute(
"""CREATE TABLE puppet (
- twid BIGINT PRIMARY KEY,
- name VARCHAR(255),
- photo_url VARCHAR(255),
- photo_mxc VARCHAR(255),
+ twid BIGINT PRIMARY KEY,
+ name VARCHAR(255),
+ photo_url VARCHAR(255),
+ photo_mxc VARCHAR(255),
- is_registered BOOLEAN NOT NULL DEFAULT false,
+ is_registered BOOLEAN NOT NULL DEFAULT false,
- custom_mxid VARCHAR(255),
- access_token TEXT,
- next_batch VARCHAR(255)
- )"""
+ custom_mxid VARCHAR(255),
+ access_token TEXT,
+ next_batch VARCHAR(255)
+ )"""
)
await conn.execute(
"""CREATE TABLE user_portal (
- "user" BIGINT,
- portal VARCHAR(255),
- portal_receiver BIGINT,
- in_community BOOLEAN NOT NULL DEFAULT false,
+ "user" BIGINT,
+ portal VARCHAR(255),
+ portal_receiver BIGINT,
+ in_community BOOLEAN NOT NULL DEFAULT false,
- FOREIGN KEY (portal, portal_receiver) REFERENCES portal(twid, receiver)
- ON UPDATE CASCADE ON DELETE CASCADE
- )"""
+ FOREIGN KEY (portal, portal_receiver) REFERENCES portal(twid, receiver)
+ ON UPDATE CASCADE ON DELETE CASCADE
+ )"""
)
await conn.execute(
"""CREATE TABLE message (
- mxid VARCHAR(255) NOT NULL,
- mx_room VARCHAR(255) NOT NULL,
- twid BIGINT,
- receiver BIGINT,
+ mxid VARCHAR(255) NOT NULL,
+ mx_room VARCHAR(255) NOT NULL,
+ twid BIGINT,
+ receiver BIGINT,
- PRIMARY KEY (twid, receiver),
- UNIQUE (mxid, mx_room)
- )"""
+ PRIMARY KEY (twid, receiver),
+ UNIQUE (mxid, mx_room)
+ )"""
)
await conn.execute(
- "CREATE TYPE twitter_reaction_key AS ENUM ('funny', 'surprised', 'sad',"
- " 'like', 'excited', 'agree',"
- " 'disagree')"
+ """CREATE TYPE twitter_reaction_key AS ENUM (
+ 'funny', 'surprised', 'sad', 'like', 'excited', 'agree', 'disagree'
+ )"""
)
await conn.execute(
"""CREATE TABLE reaction (
- mxid VARCHAR(255) NOT NULL,
- mx_room VARCHAR(255) NOT NULL,
- tw_msgid BIGINT,
- tw_receiver BIGINT,
- tw_sender BIGINT,
- reaction twitter_reaction_key NOT NULL,
+ mxid VARCHAR(255) NOT NULL,
+ mx_room VARCHAR(255) NOT NULL,
+ tw_msgid BIGINT,
+ tw_receiver BIGINT,
+ tw_sender BIGINT,
+ reaction twitter_reaction_key NOT NULL,
- PRIMARY KEY (tw_msgid, tw_receiver, tw_sender),
- FOREIGN KEY (tw_msgid, tw_receiver) REFERENCES message(twid, receiver)
- ON DELETE CASCADE ON UPDATE CASCADE,
- UNIQUE (mxid, mx_room)
- )"""
+ PRIMARY KEY (tw_msgid, tw_receiver, tw_sender),
+ FOREIGN KEY (tw_msgid, tw_receiver) REFERENCES message(twid, receiver)
+ ON DELETE CASCADE ON UPDATE CASCADE,
+ UNIQUE (mxid, mx_room)
+ )"""
)
diff --git a/mautrix_twitter/db/user.py b/mautrix_twitter/db/user.py
index 1e7b7b8..c526c29 100644
--- a/mautrix_twitter/db/user.py
+++ b/mautrix_twitter/db/user.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,14 +13,16 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, ClassVar, List, Optional
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
from attr import dataclass
from mautrix.types import RoomID, UserID
from mautrix.util.async_db import Database
-fake_db = Database("") if TYPE_CHECKING else None
+fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
@@ -28,19 +30,15 @@ class User:
db: ClassVar[Database] = fake_db
mxid: UserID
- twid: Optional[int]
- auth_token: Optional[str]
- csrf_token: Optional[str]
- poll_cursor: Optional[str]
- notice_room: Optional[RoomID]
+ twid: int | None
+ auth_token: str | None
+ csrf_token: str | None
+ poll_cursor: str | None
+ notice_room: RoomID | None
- async def insert(self) -> None:
- q = (
- 'INSERT INTO "user" (mxid, twid, auth_token, csrf_token, poll_cursor, notice_room) '
- "VALUES ($1, $2, $3, $4, $5, $6)"
- )
- await self.db.execute(
- q,
+ @property
+ def _values(self):
+ return (
self.mxid,
self.twid,
self.auth_token,
@@ -49,21 +47,23 @@ async def insert(self) -> None:
self.notice_room,
)
+ async def insert(self) -> None:
+ q = (
+ 'INSERT INTO "user" (mxid, twid, auth_token, csrf_token, poll_cursor, notice_room) '
+ "VALUES ($1, $2, $3, $4, $5, $6)"
+ )
+ await self.db.execute(q, *self._values)
+
async def update(self) -> None:
- await self.db.execute(
+ q = (
'UPDATE "user" SET twid=$2, auth_token=$3, csrf_token=$4,'
" poll_cursor=$5, notice_room=$6 "
- "WHERE mxid=$1",
- self.mxid,
- self.twid,
- self.auth_token,
- self.csrf_token,
- self.poll_cursor,
- self.notice_room,
+ "WHERE mxid=$1"
)
+ await self.db.execute(q, *self._values)
@classmethod
- async def get_by_mxid(cls, mxid: UserID) -> Optional["User"]:
+ async def get_by_mxid(cls, mxid: UserID) -> User | None:
q = (
"SELECT mxid, twid, auth_token, csrf_token, poll_cursor, notice_room "
'FROM "user" WHERE mxid=$1'
@@ -74,7 +74,7 @@ async def get_by_mxid(cls, mxid: UserID) -> Optional["User"]:
return cls(**row)
@classmethod
- async def get_by_twid(cls, twid: int) -> Optional["User"]:
+ async def get_by_twid(cls, twid: int) -> User | None:
q = (
"SELECT mxid, twid, auth_token, csrf_token, poll_cursor, notice_room "
'FROM "user" WHERE twid=$1'
@@ -85,7 +85,7 @@ async def get_by_twid(cls, twid: int) -> Optional["User"]:
return cls(**row)
@classmethod
- async def all_logged_in(cls) -> List["User"]:
+ async def all_logged_in(cls) -> list[User]:
q = (
"SELECT mxid, twid, auth_token, csrf_token, poll_cursor, notice_room "
'FROM "user" WHERE twid IS NOT NULL AND auth_token IS NOT NULL'
diff --git a/mautrix_twitter/example-config.yaml b/mautrix_twitter/example-config.yaml
index d39fd87..7fe8cdc 100644
--- a/mautrix_twitter/example-config.yaml
+++ b/mautrix_twitter/example-config.yaml
@@ -61,12 +61,6 @@ appservice:
bot_displayname: Twitter bridge bot
bot_avatar: mxc://maunium.net/HVHcnusJkQcpVcsVGZRELLCn
- # Community ID for bridged users (changes registration file) and rooms.
- # Must be created manually.
- #
- # Example: "+twitter:example.com". Set to false to disable.
- community_id: false
-
# Whether or not to receive ephemeral events via appservice transactions.
# Requires MSC2409 support (i.e. Synapse 1.22+).
# You should disable bridge -> sync_with_custom_puppets when this is enabled.
diff --git a/mautrix_twitter/formatter.py b/mautrix_twitter/formatter.py
index 9ce8af8..f318753 100644
--- a/mautrix_twitter/formatter.py
+++ b/mautrix_twitter/formatter.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2021 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,6 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Union
import html
from mautrix.types import Format, MessageType, TextMessageEventContent
@@ -21,8 +20,6 @@
from . import puppet as pu
-MessageEntity = Union[MessageEntityURL, MessageEntityUserMention]
-
async def twitter_to_matrix(message: MessageData) -> TextMessageEventContent:
content = TextMessageEventContent(
@@ -54,7 +51,7 @@ async def twitter_to_matrix(message: MessageData) -> TextMessageEventContent:
text = content.formatted_body[start:end][0] + entity.text
content.formatted_body = (
f"{content.formatted_body[:start]}"
- f'{text}'
+ f'{text}'
f"{content.formatted_body[end:]}"
)
if content.formatted_body == content.body:
diff --git a/mautrix_twitter/matrix.py b/mautrix_twitter/matrix.py
index e63eda3..687b81a 100644
--- a/mautrix_twitter/matrix.py
+++ b/mautrix_twitter/matrix.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, List, Union
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
from mautrix.bridge import BaseMatrixHandler
from mautrix.types import (
@@ -48,14 +50,13 @@ def __init__(self, bridge: "TwitterBridge") -> None:
super().__init__(bridge=bridge)
- async def send_welcome_message(self, room_id: RoomID, inviter: "u.User") -> None:
+ async def send_welcome_message(self, room_id: RoomID, inviter: u.User) -> None:
await super().send_welcome_message(room_id, inviter)
if not inviter.notice_room:
inviter.notice_room = room_id
await inviter.update()
await self.az.intent.send_notice(
- room_id,
- "This room has been marked as your " "Twitter DM bridge notice room.",
+ room_id, "This room has been marked as your Twitter DM bridge notice room."
)
async def handle_leave(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
@@ -110,11 +111,7 @@ async def handle_reaction(
)
async def handle_read_receipt(
- self,
- user: "u.User",
- portal: "po.Portal",
- event_id: EventID,
- data: SingleReceiptEventContent,
+ self, user: u.User, portal: po.Portal, event_id: EventID, data: SingleReceiptEventContent
) -> None:
message = await DBMessage.get_by_mxid(event_id, portal.mxid)
if not message:
@@ -123,7 +120,7 @@ async def handle_read_receipt(
await user.client.conversation(portal.twid).mark_read(message.twid)
@staticmethod
- async def handle_typing(room_id: RoomID, typing: List[UserID]) -> None:
+ async def handle_typing(room_id: RoomID, typing: list[UserID]) -> None:
# TODO implement
pass
@@ -136,7 +133,7 @@ async def handle_event(self, evt: Event) -> None:
await self.handle_reaction(evt.room_id, evt.sender, evt.event_id, evt.content)
async def handle_ephemeral_event(
- self, evt: Union[ReceiptEvent, PresenceEvent, TypingEvent]
+ self, evt: ReceiptEvent | PresenceEvent | TypingEvent
) -> None:
if evt.type == EventType.TYPING:
await self.handle_typing(evt.room_id, evt.content.user_ids)
diff --git a/mautrix_twitter/portal.py b/mautrix_twitter/portal.py
index df02afd..e7968d8 100644
--- a/mautrix_twitter/portal.py
+++ b/mautrix_twitter/portal.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2021 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,21 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import (
- TYPE_CHECKING,
- Any,
- AsyncGenerator,
- Awaitable,
- Deque,
- Dict,
- List,
- NamedTuple,
- Optional,
- Set,
- Tuple,
- Union,
- cast,
-)
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, NamedTuple, cast
from collections import deque
from datetime import datetime
import asyncio
@@ -82,34 +70,33 @@
StateBridge = EventType.find("m.bridge", EventType.Class.STATE)
StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE)
-BackfillEntryTypes = Union[MessageEntry, ReactionCreateEntry, ReactionDeleteEntry]
-ReuploadedMediaInfo = NamedTuple(
- "ReuploadedMediaInfo",
- mxc=Optional[ContentURI],
- decryption_info=Optional[EncryptedFile],
- mime_type=str,
- file_name=str,
- size=int,
-)
+
+
+class ReuploadedMediaInfo(NamedTuple):
+ mxc: ContentURI | None
+ decryption_info: EncryptedFile | None
+ mime_type: str
+ file_name: str
+ size: int
class Portal(DBPortal, BasePortal):
- by_mxid: Dict[RoomID, "Portal"] = {}
- by_twid: Dict[Tuple[str, int], "Portal"] = {}
+ by_mxid: dict[RoomID, Portal] = {}
+ by_twid: dict[tuple[str, int], Portal] = {}
config: Config
matrix: "m.MatrixHandler"
az: AppService
private_chat_portal_meta: bool
- _main_intent: Optional[IntentAPI]
+ _main_intent: IntentAPI | None
_create_room_lock: asyncio.Lock
backfill_lock: SimpleLock
- _msgid_dedup: Deque[int]
- _reqid_dedup: Set[str]
- _reaction_dedup: Deque[Tuple[int, int, ReactionKey]]
+ _msgid_dedup: deque[int]
+ _reqid_dedup: set[str]
+ _reaction_dedup: deque[tuple[int, int, ReactionKey]]
_main_intent: IntentAPI
- _last_participant_update: Set[int]
+ _last_participant_update: set[int]
_reaction_lock: asyncio.Lock
def __init__(
@@ -117,9 +104,9 @@ def __init__(
twid: str,
receiver: int,
conv_type: ConversationType,
- other_user: Optional[int] = None,
- mxid: Optional[RoomID] = None,
- name: Optional[str] = None,
+ other_user: int | None = None,
+ mxid: RoomID | None = None,
+ name: str | None = None,
encrypted: bool = False,
) -> None:
super().__init__(twid, receiver, conv_type, other_user, mxid, name, encrypted)
@@ -172,7 +159,7 @@ async def _upsert_reaction(
intent: IntentAPI,
mxid: EventID,
message: DBMessage,
- sender: Union["u.User", "p.Puppet"],
+ sender: u.User | p.Puppet,
reaction: ReactionKey,
) -> None:
if existing:
@@ -208,7 +195,7 @@ def _status_from_exception(self, e: Exception) -> MessageSendCheckpointStatus:
return MessageSendCheckpointStatus.PERM_FAILURE
async def handle_matrix_message(
- self, sender: "u.User", message: MessageEventContent, event_id: EventID
+ self, sender: u.User, message: MessageEventContent, event_id: EventID
) -> None:
if not sender.client:
self.log.debug(f"Ignoring message {event_id} as user is not connected")
@@ -228,7 +215,7 @@ async def handle_matrix_message(
await self._send_error_notice("Your message may not have been bridged", e)
async def _handle_matrix_message(
- self, sender: "u.User", message: MessageEventContent, event_id: EventID
+ self, sender: u.User, message: MessageEventContent, event_id: EventID
) -> None:
request_id = str(sender.client.new_request_id())
self._reqid_dedup.add(request_id)
@@ -270,11 +257,7 @@ async def _handle_matrix_message(
self.log.debug(f"Handled Matrix message {event_id} -> {resp_msg_id}")
async def handle_matrix_reaction(
- self,
- sender: "u.User",
- event_id: EventID,
- reacting_to: EventID,
- reaction_val: str,
+ self, sender: u.User, event_id: EventID, reacting_to: EventID, reaction_val: str
) -> None:
try:
await self._handle_matrix_reaction(sender, event_id, reacting_to, reaction_val)
@@ -285,11 +268,7 @@ async def handle_matrix_reaction(
await self._send_error_notice(f"Failed to react to {event_id}", e)
async def _handle_matrix_reaction(
- self,
- sender: "u.User",
- event_id: EventID,
- reacting_to: EventID,
- reaction_val: str,
+ self, sender: u.User, event_id: EventID, reacting_to: EventID, reaction_val: str
) -> None:
reaction = ReactionKey.from_emoji(reaction_val)
message = await DBMessage.get_by_mxid(reacting_to, self.mxid)
@@ -318,7 +297,7 @@ async def _handle_matrix_reaction(
self.log.debug(f"{sender.mxid} reacted to {message.twid} with {reaction}")
async def handle_matrix_redaction(
- self, sender: "u.User", event_id: EventID, redaction_event_id: EventID
+ self, sender: u.User, event_id: EventID, redaction_event_id: EventID
) -> None:
try:
await self._handle_matrix_redaction(sender, event_id)
@@ -337,7 +316,7 @@ async def handle_matrix_redaction(
)
await self._send_delivery_receipt(redaction_event_id)
- async def _handle_matrix_redaction(self, sender: "u.User", event_id: EventID) -> None:
+ async def _handle_matrix_redaction(self, sender: u.User, event_id: EventID) -> None:
assert self.mxid, "MXID is None"
async with self._reaction_lock:
@@ -358,7 +337,7 @@ async def _handle_matrix_redaction(self, sender: "u.User", event_id: EventID) ->
raise NotImplementedError("Message redactions are not supported.")
- async def handle_matrix_leave(self, user: "u.User") -> None:
+ async def handle_matrix_leave(self, user: u.User) -> None:
if self.is_direct:
self.log.info(f"{user.mxid} left private chat portal with {self.twid}")
if user.twid == self.receiver:
@@ -374,11 +353,7 @@ async def handle_matrix_leave(self, user: "u.User") -> None:
# region Twitter event handling
async def handle_twitter_message(
- self,
- source: "u.User",
- sender: "p.Puppet",
- message: MessageData,
- request_id: str,
+ self, source: u.User, sender: p.Puppet, message: MessageData, request_id: str
) -> None:
msg_id = int(message.id)
if request_id in self._reqid_dedup:
@@ -432,8 +407,8 @@ def _is_better_mime(best: VideoVariant, current: VideoVariant) -> bool:
return current_quality > best_quality
async def _handle_twitter_attachment(
- self, source: "u.User", sender: "p.Puppet", message: MessageData
- ) -> Optional[MediaMessageEventContent]:
+ self, source: u.User, sender: p.Puppet, message: MessageData
+ ) -> MediaMessageEventContent | None:
content = None
intent = sender.intent_for(self)
media = message.attachment.media
@@ -496,7 +471,7 @@ async def _handle_twitter_attachment(
return content
async def _reupload_twitter_media(
- self, source: "u.User", url: str, intent: IntentAPI
+ self, source: u.User, url: str, intent: IntentAPI
) -> ReuploadedMediaInfo:
file_name = URL(url).name
data, mime_type = await source.client.download_media(url)
@@ -520,7 +495,7 @@ async def _reupload_twitter_media(
return ReuploadedMediaInfo(mxc, decryption_info, mime_type, file_name, len(data))
async def handle_twitter_reaction_add(
- self, sender: "p.Puppet", msg_id: int, reaction: ReactionKey, time: datetime
+ self, sender: p.Puppet, msg_id: int, reaction: ReactionKey, time: datetime
) -> None:
async with self._reaction_lock:
dedup_id = (msg_id, sender.twid, reaction)
@@ -543,7 +518,7 @@ async def handle_twitter_reaction_add(
await self._upsert_reaction(existing, intent, mxid, message, sender, reaction)
async def handle_twitter_reaction_remove(
- self, sender: "p.Puppet", msg_id: int, key: ReactionKey
+ self, sender: p.Puppet, msg_id: int, key: ReactionKey
) -> None:
reaction = await DBReaction.get_by_twid(msg_id, self.receiver, sender.twid)
if reaction and reaction.reaction == key:
@@ -555,7 +530,7 @@ async def handle_twitter_reaction_remove(
self.log.debug(f"Removed {reaction} after Twitter removal")
async def handle_twitter_receipt(
- self, sender: "p.Puppet", read_up_to: int, historical: bool = False
+ self, sender: p.Puppet, read_up_to: int, historical: bool = False
) -> None:
message = await DBMessage.get_by_twid(read_up_to, self.receiver)
if not message:
@@ -613,7 +588,7 @@ async def _update_name(self, name: str) -> bool:
return True
return False
- async def _update_participants(self, participants: List[Participant]) -> None:
+ async def _update_participants(self, participants: list[Participant]) -> None:
if not self.mxid:
return
@@ -647,7 +622,7 @@ async def _update_participants(self, participants: List[Participant]) -> None:
# endregion
# region Backfilling
- async def backfill(self, source: "u.User", is_initial: bool = False) -> None:
+ async def backfill(self, source: u.User, is_initial: bool = False) -> None:
if not is_initial:
raise RuntimeError("Non-initial backfilling is not supported")
limit = self.config["bridge.backfill.initial_limit"]
@@ -658,7 +633,7 @@ async def backfill(self, source: "u.User", is_initial: bool = False) -> None:
with self.backfill_lock:
await self._backfill(source, limit)
- async def _backfill(self, source: "u.User", limit: int) -> None:
+ async def _backfill(self, source: u.User, limit: int) -> None:
self.log.debug("Backfilling history through %s", source.mxid)
entries = await self._fetch_backfill_entries(source, limit)
@@ -678,8 +653,8 @@ async def _backfill(self, source: "u.User", limit: int) -> None:
self.log.info("Backfilled %d messages through %s", len(entries), source.mxid)
async def _fetch_backfill_entries(
- self, source: "u.User", limit: int
- ) -> List[BackfillEntryTypes]:
+ self, source: u.User, limit: int
+ ) -> list[MessageEntry | ReactionCreateEntry | ReactionDeleteEntry]:
conv = source.client.conversation(self.twid)
entries = []
self.log.debug("Fetching up to %d messages through %s", limit, source.twid)
@@ -708,7 +683,7 @@ async def _fetch_backfill_entries(
if isinstance(entry, (MessageEntry, ReactionCreateEntry, ReactionDeleteEntry))
]
- async def _invite_own_puppet_backfill(self, source: "u.User") -> Set[IntentAPI]:
+ async def _invite_own_puppet_backfill(self, source: u.User) -> set[IntentAPI]:
backfill_leave = set()
# TODO we should probably only invite the puppet when needed
if self.config["bridge.backfill.invite_own_puppet"]:
@@ -719,7 +694,9 @@ async def _invite_own_puppet_backfill(self, source: "u.User") -> Set[IntentAPI]:
backfill_leave.add(sender.default_mxid_intent)
return backfill_leave
- async def _handle_backfill_entry(self, source: "u.User", entry: BackfillEntryTypes) -> None:
+ async def _handle_backfill_entry(
+ self, source: u.User, entry: MessageEntry | ReactionCreateEntry | ReactionDeleteEntry
+ ) -> None:
sender = await p.Puppet.get_by_twid(int(entry.sender_id))
if isinstance(entry, MessageEntry):
await self.handle_twitter_message(source, sender, entry.message_data, entry.request_id)
@@ -740,7 +717,7 @@ def bridge_info_state_key(self) -> str:
return f"net.maunium.twitter://twitter/{self.twid}"
@property
- def bridge_info(self) -> Dict[str, Any]:
+ def bridge_info(self) -> dict[str, Any]:
return {
"bridgebot": self.az.bot_mxid,
"creator": self.main_intent.mxid,
@@ -777,7 +754,7 @@ async def update_bridge_info(self) -> None:
# endregion
# region Creating Matrix rooms
- async def create_matrix_room(self, source: "u.User", info: Conversation) -> Optional[RoomID]:
+ async def create_matrix_room(self, source: u.User, info: Conversation) -> RoomID | None:
if self.mxid:
try:
await self._update_matrix_room(source, info)
@@ -787,7 +764,7 @@ async def create_matrix_room(self, source: "u.User", info: Conversation) -> Opti
async with self._create_room_lock:
return await self._create_matrix_room(source, info)
- def _get_invite_content(self, double_puppet: Optional["p.Puppet"]) -> Dict[str, Any]:
+ def _get_invite_content(self, double_puppet: p.Puppet | None) -> dict[str, Any]:
invite_content = {}
if double_puppet:
invite_content["fi.mau.will_auto_accept"] = True
@@ -795,7 +772,7 @@ def _get_invite_content(self, double_puppet: Optional["p.Puppet"]) -> Dict[str,
invite_content["is_direct"] = True
return invite_content
- async def _add_user(self, user: "u.User") -> None:
+ async def _add_user(self, user: u.User) -> None:
puppet = await p.Puppet.get_by_custom_mxid(user.mxid)
await self.main_intent.invite_user(
self.mxid,
@@ -808,27 +785,17 @@ async def _add_user(self, user: "u.User") -> None:
if did_join and self.is_direct:
await user.update_direct_chats({self.main_intent.mxid: [self.mxid]})
- async def _update_matrix_room(self, source: "u.User", info: Conversation) -> None:
+ async def _update_matrix_room(self, source: u.User, info: Conversation) -> None:
await self._add_user(source)
await self.update_info(info)
- # TODO
- # up = DBUserPortal.get(source.fbid, self.fbid, self.fb_receiver)
- # if not up:
- # in_community = await source._community_helper.add_room(source._community_id, self.mxid)
- # DBUserPortal(user=source.fbid, portal=self.fbid, portal_receiver=self.fb_receiver,
- # in_community=in_community).insert()
- # elif not up.in_community:
- # in_community = await source._community_helper.add_room(source._community_id, self.mxid)
- # up.edit(in_community=in_community)
-
- async def _create_matrix_room(self, source: "u.User", info: Conversation) -> Optional[RoomID]:
+ async def _create_matrix_room(self, source: u.User, info: Conversation) -> RoomID | None:
if self.mxid:
await self._update_matrix_room(source, info)
return self.mxid
await self.update_info(info)
self.log.debug("Creating Matrix room")
- name: Optional[str] = None
+ name: str | None = None
initial_state = [
{
"type": str(StateBridge),
@@ -855,13 +822,6 @@ async def _create_matrix_room(self, source: "u.User", info: Conversation) -> Opt
invites.append(self.az.bot_mxid)
if self.encrypted or self.private_chat_portal_meta or not self.is_direct:
name = self.name
- if self.config["appservice.community_id"]:
- initial_state.append(
- {
- "type": "m.room.related_groups",
- "content": {"groups": [self.config["appservice.community_id"]]},
- }
- )
# We lock backfill lock here so any messages that come between the room being created
# and the initial backfill finishing wouldn't be bridged before the backfill messages.
@@ -883,9 +843,7 @@ async def _create_matrix_room(self, source: "u.User", info: Conversation) -> Opt
try:
await self.az.intent.ensure_joined(self.mxid)
except Exception:
- self.log.warning(
- "Failed to add bridge bot " f"to new private chat {self.mxid}"
- )
+ self.log.warning(f"Failed to add bridge bot to new private chat {self.mxid}")
await self.update()
self.log.debug(f"Matrix room created: {self.mxid}")
@@ -901,8 +859,7 @@ async def _create_matrix_room(self, source: "u.User", info: Conversation) -> Opt
await source.update_direct_chats({self.main_intent.mxid: [self.mxid]})
except MatrixError:
self.log.debug(
- "Failed to join custom puppet into newly created portal",
- exc_info=True,
+ "Failed to join custom puppet into newly created portal", exc_info=True
)
if not info.trusted:
@@ -911,11 +868,6 @@ async def _create_matrix_room(self, source: "u.User", info: Conversation) -> Opt
msg += ' Note: Twitter has marked this as a "low quality" message.'
await self.main_intent.send_notice(self.mxid, msg)
- # TODO
- # in_community = await source._community_helper.add_room(source._community_id, self.mxid)
- # DBUserPortal(user=source.fbid, portal=self.fbid, portal_receiver=self.fb_receiver,
- # in_community=in_community).upsert()
-
try:
await self.backfill(source, is_initial=True)
except Exception:
@@ -950,17 +902,15 @@ async def save(self) -> None:
await self.update()
@classmethod
- def all_with_room(cls) -> AsyncGenerator["Portal", None]:
+ def all_with_room(cls) -> AsyncGenerator[Portal, None]:
return cls._db_to_portals(super().all_with_room())
@classmethod
- def find_private_chats_with(cls, other_user: int) -> AsyncGenerator["Portal", None]:
+ def find_private_chats_with(cls, other_user: int) -> AsyncGenerator[Portal, None]:
return cls._db_to_portals(super().find_private_chats_with(other_user))
@classmethod
- async def _db_to_portals(
- cls, query: Awaitable[List["Portal"]]
- ) -> AsyncGenerator["Portal", None]:
+ async def _db_to_portals(cls, query: Awaitable[list[Portal]]) -> AsyncGenerator[Portal, None]:
portals = await query
for index, portal in enumerate(portals):
try:
@@ -971,7 +921,7 @@ async def _db_to_portals(
@classmethod
@async_getter_lock
- async def get_by_mxid(cls, mxid: RoomID) -> Optional["Portal"]:
+ async def get_by_mxid(cls, mxid: RoomID) -> Portal | None:
try:
return cls.by_mxid[mxid]
except KeyError:
@@ -991,8 +941,8 @@ async def get_by_twid(
twid: str,
*,
receiver: int = 0,
- conv_type: Optional[ConversationType] = None,
- ) -> Optional["Portal"]:
+ conv_type: ConversationType | None = None,
+ ) -> Portal | None:
if conv_type == ConversationType.GROUP_DM and receiver != 0:
receiver = 0
try:
diff --git a/mautrix_twitter/puppet.py b/mautrix_twitter/puppet.py
index ca5cbd3..a13c2b4 100644
--- a/mautrix_twitter/puppet.py
+++ b/mautrix_twitter/puppet.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import TYPE_CHECKING, AsyncGenerator, AsyncIterable, Awaitable, Dict, Optional, cast
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, AsyncGenerator, AsyncIterable, Awaitable, cast
from os import path
from aiohttp import ClientSession
@@ -34,8 +36,8 @@
class Puppet(DBPuppet, BasePuppet):
- by_twid: Dict[int, "Puppet"] = {}
- by_custom_mxid: Dict[UserID, "Puppet"] = {}
+ by_twid: dict[int, Puppet] = {}
+ by_custom_mxid: dict[UserID, Puppet] = {}
hs_domain: str
mxid_template: SimpleTemplate[int]
@@ -47,14 +49,14 @@ class Puppet(DBPuppet, BasePuppet):
def __init__(
self,
twid: int,
- name: Optional[str] = None,
- photo_url: Optional[str] = None,
- photo_mxc: Optional[ContentURI] = None,
+ name: str | None = None,
+ photo_url: str | None = None,
+ photo_mxc: ContentURI | None = None,
is_registered: bool = False,
- custom_mxid: Optional[UserID] = None,
- access_token: Optional[str] = None,
- next_batch: Optional[SyncToken] = None,
- base_url: Optional[URL] = None,
+ custom_mxid: UserID | None = None,
+ access_token: str | None = None,
+ next_batch: SyncToken | None = None,
+ base_url: URL | None = None,
) -> None:
super().__init__(
twid=twid,
@@ -100,7 +102,7 @@ def init_cls(cls, bridge: "TwitterBridge") -> AsyncIterable[Awaitable[None]]:
cls.login_device_name = "Twitter DM Bridge"
return (puppet.try_start() async for puppet in cls.all_with_custom_mxid())
- def intent_for(self, portal: "p.Portal") -> IntentAPI:
+ def intent_for(self, portal: p.Portal) -> IntentAPI:
if portal.other_user == self.twid or (
self.config["bridge.backfill.invite_own_puppet"] and portal.backfill_lock.locked
):
@@ -164,7 +166,7 @@ async def save(self) -> None:
await self.update()
@classmethod
- async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Optional["Puppet"]:
+ async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Puppet | None:
twid = cls.get_id_from_mxid(mxid)
if twid:
return await cls.get_by_twid(twid, create=create)
@@ -172,7 +174,7 @@ async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Optional["Puppe
@classmethod
@async_getter_lock
- async def get_by_custom_mxid(cls, mxid: UserID) -> Optional["Puppet"]:
+ async def get_by_custom_mxid(cls, mxid: UserID) -> Puppet | None:
try:
return cls.by_custom_mxid[mxid]
except KeyError:
@@ -186,7 +188,7 @@ async def get_by_custom_mxid(cls, mxid: UserID) -> Optional["Puppet"]:
return None
@classmethod
- def get_id_from_mxid(cls, mxid: UserID) -> Optional[int]:
+ def get_id_from_mxid(cls, mxid: UserID) -> int | None:
return cls.mxid_template.parse(mxid)
@classmethod
@@ -195,7 +197,7 @@ def get_mxid_from_id(cls, twid: int) -> UserID:
@classmethod
@async_getter_lock
- async def get_by_twid(cls, twid: int, *, create: bool = True) -> Optional["Puppet"]:
+ async def get_by_twid(cls, twid: int, *, create: bool = True) -> Puppet | None:
try:
return cls.by_twid[twid]
except KeyError:
@@ -215,7 +217,7 @@ async def get_by_twid(cls, twid: int, *, create: bool = True) -> Optional["Puppe
return None
@classmethod
- async def all_with_custom_mxid(cls) -> AsyncGenerator["Puppet", None]:
+ async def all_with_custom_mxid(cls) -> AsyncGenerator[Puppet, None]:
puppets = await super().all_with_custom_mxid()
puppet: cls
for index, puppet in enumerate(puppets):
diff --git a/mautrix_twitter/user.py b/mautrix_twitter/user.py
index 1a81267..0f76cba 100644
--- a/mautrix_twitter/user.py
+++ b/mautrix_twitter/user.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,17 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import (
- TYPE_CHECKING,
- AsyncGenerator,
- AsyncIterable,
- Awaitable,
- Dict,
- List,
- Optional,
- Union,
- cast,
-)
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, AsyncGenerator, AsyncIterable, Awaitable, cast
import asyncio
import logging
@@ -71,28 +63,28 @@
class User(DBUser, BaseUser):
- by_mxid: Dict[UserID, "User"] = {}
- by_twid: Dict[int, "User"] = {}
+ by_mxid: dict[UserID, User] = {}
+ by_twid: dict[int, User] = {}
config: Config
- client: Optional[TwitterAPI]
+ client: TwitterAPI | None
permission_level: str
- username: Optional[str]
+ username: str | None
_notice_room_lock: asyncio.Lock
_notice_send_lock: asyncio.Lock
- _is_logged_in: Optional[bool]
+ _is_logged_in: bool | None
_connected: bool
def __init__(
self,
mxid: UserID,
- twid: Optional[int] = None,
- auth_token: Optional[str] = None,
- csrf_token: Optional[str] = None,
- poll_cursor: Optional[str] = None,
- notice_room: Optional[RoomID] = None,
+ twid: int | None = None,
+ auth_token: str | None = None,
+ csrf_token: str | None = None,
+ poll_cursor: str | None = None,
+ notice_room: RoomID | None = None,
) -> None:
super().__init__(
mxid=mxid,
@@ -140,7 +132,7 @@ async def is_logged_in(self, ignore_cache: bool = False) -> bool:
self._is_logged_in = False
return self.client and self._is_logged_in
- async def get_puppet(self) -> Optional["pu.Puppet"]:
+ async def get_puppet(self) -> pu.Puppet | None:
if not self.twid:
return None
return await pu.Puppet.get_by_twid(self.twid)
@@ -168,9 +160,7 @@ async def try_connect(self) -> None:
BridgeStateEvent.UNKNOWN_ERROR, error="twitter-connection-failed"
)
- async def connect(
- self, auth_token: Optional[str] = None, csrf_token: Optional[str] = None
- ) -> None:
+ async def connect(self, auth_token: str | None = None, csrf_token: str | None = None) -> None:
client = TwitterAPI(
log=logging.getLogger("mau.twitter.api").getChild(self.mxid),
loop=self.loop,
@@ -217,7 +207,7 @@ async def fill_bridge_state(self, state: BridgeState) -> None:
puppet = await pu.Puppet.get_by_twid(self.twid)
state.remote_name = puppet.name
- async def get_bridge_states(self) -> List[BridgeState]:
+ async def get_bridge_states(self) -> list[BridgeState]:
if not self.twid:
return []
state = BridgeState(state_event=BridgeStateEvent.UNKNOWN_ERROR)
@@ -225,12 +215,12 @@ async def get_bridge_states(self) -> List[BridgeState]:
state.state_event = BridgeStateEvent.CONNECTED
return [state]
- async def on_connect(self, evt: Union[PollingStarted, PollingErrorResolved]) -> None:
+ async def on_connect(self, evt: PollingStarted | PollingErrorResolved) -> None:
self._track_metric(METRIC_CONNECTED, True)
self._connected = True
await self.push_bridge_state(BridgeStateEvent.CONNECTED)
- async def on_disconnect(self, evt: Union[PollingStopped, PollingErrored]) -> None:
+ async def on_disconnect(self, evt: PollingStopped | PollingErrored) -> None:
self._track_metric(METRIC_CONNECTED, False)
self._connected = False
if isinstance(evt, PollingStopped):
@@ -261,10 +251,10 @@ async def get_notice_room(self) -> RoomID:
async def send_bridge_notice(
self,
text: str,
- edit: Optional[EventID] = None,
- state_event: Optional[BridgeStateEvent] = None,
+ edit: EventID | None = None,
+ state_event: BridgeStateEvent | None = None,
important: bool = False,
- ) -> Optional[EventID]:
+ ) -> EventID | None:
if state_event:
await self.push_bridge_state(state_event, message=text)
if self.config["bridge.disable_bridge_notices"]:
@@ -332,7 +322,7 @@ async def _try_initial_sync(self) -> None:
self.log.debug("Initial sync completed, starting polling")
self.client.start_polling()
- async def get_direct_chats(self) -> Dict[UserID, List[RoomID]]:
+ async def get_direct_chats(self) -> dict[UserID, list[RoomID]]:
return {
pu.Puppet.get_mxid_from_id(portal.other_user): [portal.mxid]
for portal in await DBPortal.find_private_chats_of(self.twid)
@@ -424,7 +414,7 @@ async def handle_message(self, evt: MessageEntry) -> None:
await portal.handle_twitter_message(self, sender, evt.message_data, evt.request_id)
@async_time(METRIC_REACTION)
- async def handle_reaction(self, evt: Union[ReactionCreateEntry, ReactionDeleteEntry]) -> None:
+ async def handle_reaction(self, evt: ReactionCreateEntry | ReactionDeleteEntry) -> None:
portal = await po.Portal.get_by_twid(
evt.conversation_id, receiver=self.twid, conv_type=evt.conversation.type
)
@@ -461,7 +451,7 @@ def _add_to_cache(self) -> None:
@classmethod
@async_getter_lock
- async def get_by_mxid(cls, mxid: UserID, *, create: bool = True) -> Optional["User"]:
+ async def get_by_mxid(cls, mxid: UserID, *, create: bool = True) -> User | None:
# Never allow ghosts to be users
if pu.Puppet.get_id_from_mxid(mxid):
return None
@@ -485,7 +475,7 @@ async def get_by_mxid(cls, mxid: UserID, *, create: bool = True) -> Optional["Us
@classmethod
@async_getter_lock
- async def get_by_twid(cls, twid: int) -> Optional["User"]:
+ async def get_by_twid(cls, twid: int) -> User | None:
try:
return cls.by_twid[twid]
except KeyError:
@@ -499,7 +489,7 @@ async def get_by_twid(cls, twid: int) -> Optional["User"]:
return None
@classmethod
- async def all_logged_in(cls) -> AsyncGenerator["User", None]:
+ async def all_logged_in(cls) -> AsyncGenerator[User, None]:
users = await super().all_logged_in()
user: cls
for index, user in enumerate(users):
diff --git a/mautrix_twitter/web/provisioning_api.py b/mautrix_twitter/web/provisioning_api.py
index f5db7cf..1b2c67e 100644
--- a/mautrix_twitter/web/provisioning_api.py
+++ b/mautrix_twitter/web/provisioning_api.py
@@ -1,5 +1,5 @@
# mautrix-twitter - A Matrix-Twitter DM puppeting bridge
-# Copyright (C) 2020 Tulir Asokan
+# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Awaitable, Dict
+from __future__ import annotations
+
+from typing import Awaitable
import json
import logging
@@ -38,7 +40,7 @@ def __init__(self, shared_secret: str) -> None:
self.app.router.add_post("/api/logout", self.logout)
@property
- def _acao_headers(self) -> Dict[str, str]:
+ def _acao_headers(self) -> dict[str, str]:
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
@@ -46,7 +48,7 @@ def _acao_headers(self) -> Dict[str, str]:
}
@property
- def _headers(self) -> Dict[str, str]:
+ def _headers(self) -> dict[str, str]:
return {
**self._acao_headers,
"Content-Type": "application/json",
@@ -55,7 +57,7 @@ def _headers(self) -> Dict[str, str]:
async def login_options(self, _: web.Request) -> web.Response:
return web.Response(status=200, headers=self._headers)
- def check_token(self, request: web.Request) -> Awaitable["u.User"]:
+ def check_token(self, request: web.Request) -> Awaitable[u.User]:
try:
token = request.headers["Authorization"]
token = token[len("Bearer ") :]
diff --git a/mautwitdm/conversation.py b/mautwitdm/conversation.py
index 100e137..f1a759b 100644
--- a/mautwitdm/conversation.py
+++ b/mautwitdm/conversation.py
@@ -1,25 +1,24 @@
-# Copyright (c) 2020 Tulir Asokan
+# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from typing import TYPE_CHECKING, Optional, Union
+from __future__ import annotations
+
from uuid import UUID
from yarl import URL
+from . import twitter as tw
from .errors import check_error
from .types import FetchConversationResponse, ReactionKey, SendResponse
-if TYPE_CHECKING:
- from .twitter import TwitterAPI
-
class Conversation:
- api: "TwitterAPI"
+ api: tw.TwitterAPI
id: str
- def __init__(self, api: "TwitterAPI", id: str) -> None:
+ def __init__(self, api: tw.TwitterAPI, id: str) -> None:
self.api = api
self.id = id
@@ -28,7 +27,7 @@ def api_url(self) -> URL:
"""The base URL for API requests related to this conversation."""
return self.api.dm_url / "conversation" / self.id
- async def mark_read(self, last_read_event_id: Union[int, str]) -> None:
+ async def mark_read(self, last_read_event_id: int | str) -> None:
"""Mark the conversation as read, up to the given event ID."""
req = {"conversationId": self.id, "last_read_event_id": str(last_read_event_id)}
url = self.api_url / "mark_read.json"
@@ -48,8 +47,8 @@ async def accept(self) -> None:
async def send(
self,
text: str,
- media_id: Optional[Union[str, int]] = None,
- request_id: Optional[Union[UUID, str]] = None,
+ media_id: str | int | None = None,
+ request_id: UUID | str | None = None,
) -> SendResponse:
"""
Send a message to this conversation.
@@ -79,7 +78,7 @@ async def send(
resp_data = await check_error(resp)
return SendResponse.deserialize(resp_data)
- async def react(self, message_id: Union[str, int], key: ReactionKey) -> None:
+ async def react(self, message_id: str | int, key: ReactionKey) -> None:
"""
React to a message. Reacting to the same message multiple times will override earlier
reactions.
@@ -98,7 +97,7 @@ async def react(self, message_id: Union[str, int], key: ReactionKey) -> None:
async with self.api.http.post(url, headers=self.api.headers) as resp:
await check_error(resp)
- async def delete_reaction(self, message_id: Union[str, int], key: ReactionKey) -> None:
+ async def delete_reaction(self, message_id: str | int, key: ReactionKey) -> None:
"""
Delete an earlier reaction.
@@ -117,7 +116,7 @@ async def delete_reaction(self, message_id: Union[str, int], key: ReactionKey) -
await check_error(resp)
async def fetch(
- self, max_id: Optional[str] = None, include_info: bool = True
+ self, max_id: str | None = None, include_info: bool = True
) -> FetchConversationResponse:
"""
Fetch the conversation metadata and message history.
diff --git a/mautwitdm/dispatcher.py b/mautwitdm/dispatcher.py
index 1f36c8a..ea8c9d7 100644
--- a/mautwitdm/dispatcher.py
+++ b/mautwitdm/dispatcher.py
@@ -1,15 +1,16 @@
-# Copyright (c) 2020 Tulir Asokan
+# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.'
-from typing import Any, Awaitable, Callable, Dict, List, Type, TypeVar
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from __future__ import annotations
+
+from typing import Any, Awaitable, Callable, TypeVar
from mautrix.util.logging import TraceLogger
T = TypeVar("T")
Handler = Callable[[T], Awaitable[Any]]
-HandlerMap = Dict[Type[T], List[Handler]]
class TwitterDispatcher:
@@ -19,7 +20,7 @@ class TwitterDispatcher:
"""
log: TraceLogger
- _handlers: HandlerMap
+ _handlers: dict[type[T], list[Handler]]
async def dispatch(self, event: T) -> None:
"""
@@ -35,7 +36,7 @@ async def dispatch(self, event: T) -> None:
except Exception:
self.log.exception(f"Error while handling event of type {type(event)}")
- def add_handler(self, event_type: Type[T], handler: Handler) -> None:
+ def add_handler(self, event_type: type[T], handler: Handler) -> None:
"""
Add an event handler.
@@ -45,7 +46,7 @@ def add_handler(self, event_type: Type[T], handler: Handler) -> None:
"""
self._handlers[event_type].append(handler)
- def remove_handler(self, event_type: Type[T], handler: Handler) -> None:
+ def remove_handler(self, event_type: type[T], handler: Handler) -> None:
"""
Remove an event handler.
diff --git a/mautwitdm/poller.py b/mautwitdm/poller.py
index 033d0f6..778a319 100644
--- a/mautwitdm/poller.py
+++ b/mautwitdm/poller.py
@@ -1,9 +1,10 @@
-# Copyright (c) 2020 Tulir Asokan
+# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, TypeVar, Union
+from __future__ import annotations
+
import asyncio
import logging
import time
@@ -12,15 +13,11 @@
from attr import dataclass
from yarl import URL
-from .conversation import Conversation
+from . import conversation as c
from .dispatcher import TwitterDispatcher
from .errors import RateLimitError, check_error
from .types import InitialStateResponse, PollResponse
-T = TypeVar("T")
-Handler = Callable[[T], Awaitable[Any]]
-HandlerMap = Dict[Type[T], List[Handler]]
-
class PollingStarted:
pass
@@ -49,19 +46,19 @@ class TwitterPoller(TwitterDispatcher):
log: logging.Logger
loop: asyncio.AbstractEventLoop
http: ClientSession
- headers: Dict[str, str]
+ headers: dict[str, str]
skip_poll_wait: asyncio.Event
poll_sleep: int = 3
error_sleep: int = 5
max_poll_errors: int = 12
- poll_cursor: Optional[str]
+ poll_cursor: str | None
dispatch_initial_resp: bool
- _poll_task: Optional[asyncio.Task]
- _typing_in: Optional[Conversation]
+ _poll_task: asyncio.Task | None
+ _typing_in: c.Conversation | None
@property
- def poll_params(self) -> Dict[str, str]:
+ def poll_params(self) -> dict[str, str]:
return {
"cards_platform": "Web-12",
"include_cards": "1",
@@ -78,7 +75,7 @@ def poll_params(self) -> Dict[str, str]:
}
@property
- def full_state_params(self) -> Dict[str, str]:
+ def full_state_params(self) -> dict[str, str]:
return {
"include_profile_interstitial_type": "1",
"include_blocking": "1",
@@ -162,7 +159,7 @@ async def poll_forever(self, raise_exceptions: bool = True) -> None:
if raise_exceptions:
raise
- async def dispatch_all(self, resp: Union[PollResponse, InitialStateResponse]) -> None:
+ async def dispatch_all(self, resp: PollResponse | InitialStateResponse) -> None:
for user in (resp.users or {}).values():
await self.dispatch(user)
for conversation in (resp.conversations or {}).values():
diff --git a/mautwitdm/streamer.py b/mautwitdm/streamer.py
index 967df0c..3a45205 100644
--- a/mautwitdm/streamer.py
+++ b/mautwitdm/streamer.py
@@ -1,9 +1,11 @@
-# Copyright (c) 2020 Tulir Asokan
+# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from typing import AsyncGenerator, Dict, Set
+from __future__ import annotations
+
+from typing import AsyncGenerator
import asyncio
import io
import json
@@ -29,10 +31,10 @@ class TwitterStreamer(TwitterDispatcher):
log: logging.Logger
loop: asyncio.AbstractEventLoop
http: ClientSession
- headers: Dict[str, str]
+ headers: dict[str, str]
user_agent: str
- topics: Set[str]
+ topics: set[str]
_stream_task: asyncio.Task
async def _stream(self) -> AsyncGenerator[StreamEvent, None]:
@@ -71,7 +73,7 @@ async def _stream(self) -> AsyncGenerator[StreamEvent, None]:
data = json.loads(chunk[len(data_prefix) : -len(chunk_separator)])
yield StreamEvent.deserialize(data["payload"])
- async def update_topics(self, subscribe: Set[str], unsubscribe: Set[str]) -> None:
+ async def update_topics(self, subscribe: set[str], unsubscribe: set[str]) -> None:
"""
Update the topics the client is subscribed to.
diff --git a/mautwitdm/twitter.py b/mautwitdm/twitter.py
index 9835604..14b23a3 100644
--- a/mautwitdm/twitter.py
+++ b/mautwitdm/twitter.py
@@ -3,7 +3,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from typing import Any, Dict, List, NamedTuple, Optional
+from __future__ import annotations
+
+from typing import Any, NamedTuple
from collections import defaultdict
from http.cookies import SimpleCookie
from uuid import UUID, getnode, uuid1
@@ -13,7 +15,7 @@
from aiohttp import ClientSession
from yarl import URL
-from .conversation import Conversation
+from . import conversation as c
from .errors import TwitterError, check_error
from .poller import TwitterPoller
from .streamer import TwitterStreamer
@@ -42,10 +44,10 @@ class TwitterAPI(TwitterUploader, TwitterStreamer, TwitterPoller):
def __init__(
self,
- http: Optional[ClientSession] = None,
- log: Optional[logging.Logger] = None,
- loop: Optional[asyncio.AbstractEventLoop] = None,
- node_id: Optional[int] = None,
+ http: ClientSession | None = None,
+ log: logging.Logger | None = None,
+ loop: asyncio.AbstractEventLoop | None = None,
+ node_id: int | None = None,
) -> None:
self.loop = loop or asyncio.get_event_loop()
self.http = http or ClientSession(loop=self.loop)
@@ -58,7 +60,7 @@ def __init__(
self.active = True
self._typing_in = None
self.user_agent = (
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) " "Gecko/20100101 Firefox/89.0"
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0"
)
self.skip_poll_wait = asyncio.Event()
self.topics = set()
@@ -79,7 +81,7 @@ def set_tokens(self, auth_token: str, csrf_token: str) -> None:
cookie["ct0"].update({"domain": "twitter.com", "path": "/"})
self.http.cookie_jar.update_cookies(cookie, twitter_com)
- def mark_typing(self, conversation_id: Optional[str]) -> None:
+ def mark_typing(self, conversation_id: str | None) -> None:
"""
Mark the user as typing in the specified conversation. This will make the polling task call
:meth:`Conversation.mark_typing` of the specified conversation after each poll.
@@ -90,7 +92,7 @@ def mark_typing(self, conversation_id: Optional[str]) -> None:
self._typing_in = self.conversation(conversation_id)
@property
- def tokens(self) -> Optional[Tokens]:
+ def tokens(self) -> Tokens | None:
cookies = self.http.cookie_jar.filter_cookies(URL("https://twitter.com/"))
try:
return Tokens(auth_token=cookies["auth_token"].value, csrf_token=cookies["ct0"].value)
@@ -98,7 +100,7 @@ def tokens(self) -> Optional[Tokens]:
return None
@property
- def headers(self) -> Dict[str, str]:
+ def headers(self) -> dict[str, str]:
"""
Get the headers to use with every request to Twitter.
@@ -108,8 +110,10 @@ def headers(self) -> Dict[str, str]:
csrf_token = self.http.cookie_jar.filter_cookies(twitter_com)["ct0"].value
return {
# Hardcoded authorization header from the web app
- "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs"
- "%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
+ "authorization": (
+ "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs"
+ "%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
+ ),
"User-Agent": self.user_agent,
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
@@ -142,8 +146,8 @@ def new_request_id(self) -> UUID:
"""
return uuid1(self.node_id)
- def conversation(self, id: str) -> Conversation:
- return Conversation(self, id)
+ def conversation(self, id: str) -> c.Conversation:
+ return c.Conversation(self, id)
async def update_last_seen_event_id(self, last_seen_event_id: str) -> None:
await self.http.post(
@@ -155,7 +159,7 @@ async def update_last_seen_event_id(self, last_seen_event_id: str) -> None:
headers=self.headers,
)
- async def get_user_identifier(self) -> Optional[str]:
+ async def get_user_identifier(self) -> str | None:
async with self.http.post(
self.base_url / "branch" / "init.json", json={}, headers=self.headers
) as resp:
@@ -163,7 +167,7 @@ async def get_user_identifier(self) -> Optional[str]:
resp_data = await check_error(resp)
except TwitterError as e:
# Sometimes branch/init.json returns 38: countryCode parameter is missing.
- # It still checks auth and we don't actually need this user identifier,
+ # It still checks auth, and we don't actually need this user identifier,
# so it might be safe to ignore
if e.code == 38:
self.log.warning(f"Ignoring {e} in branch/init.json request")
@@ -171,7 +175,7 @@ async def get_user_identifier(self) -> Optional[str]:
raise
return resp_data.get("user_identifier", None)
- async def get_settings(self) -> Dict[str, Any]:
+ async def get_settings(self) -> dict[str, Any]:
"""Get the account settings of the currently logged in account."""
async with self.http.get(
self.base_url / "account" / "settings.json", headers=self.headers
@@ -180,9 +184,9 @@ async def get_settings(self) -> Dict[str, Any]:
async def lookup_users(
self,
- user_ids: Optional[List[int]] = None,
- usernames: Optional[List[str]] = None,
- ) -> List[User]:
+ user_ids: list[int] | None = None,
+ usernames: list[str] | None = None,
+ ) -> list[User]:
query = {"include_entities": "false", "tweet_mode": "extended"}
if user_ids:
query["user_id"] = ",".join(str(id) for id in user_ids)
diff --git a/mautwitdm/uploader.py b/mautwitdm/uploader.py
index 30aeb15..d1829fa 100644
--- a/mautwitdm/uploader.py
+++ b/mautwitdm/uploader.py
@@ -1,9 +1,10 @@
-# Copyright (c) 2020 Tulir Asokan
+# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from typing import Dict
+from __future__ import annotations
+
import asyncio
import logging
import math
@@ -20,7 +21,7 @@ class TwitterUploader:
http: ClientSession
log: logging.Logger
- headers: Dict[str, str]
+ headers: dict[str, str]
async def _wait_processing(self, media_id: str, wait_requests: int = 1) -> MediaUploadResponse:
query_req = self.upload_url.with_query({"command": "STATUS", "media_id": media_id})