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})