chore: retab, delete discord file
This commit is contained in:
parent
461127a4ac
commit
947217abe3
9 changed files with 1106 additions and 1324 deletions
|
@ -13,191 +13,191 @@ from misc import log_except, request
|
|||
|
||||
|
||||
class AppService(bottle.Bottle):
|
||||
def __init__(self, config: dict, http: urllib3.PoolManager) -> None:
|
||||
super(AppService, self).__init__()
|
||||
def __init__(self, config: dict, http: urllib3.PoolManager) -> None:
|
||||
super(AppService, self).__init__()
|
||||
|
||||
self.as_token = config["as_token"]
|
||||
self.hs_token = config["hs_token"]
|
||||
self.base_url = config["homeserver"]
|
||||
self.server_name = config["server_name"]
|
||||
self.user_id = f"@{config['user_id']}:{self.server_name}"
|
||||
self.http = http
|
||||
self.logger = logging.getLogger("appservice")
|
||||
self.as_token = config["as_token"]
|
||||
self.hs_token = config["hs_token"]
|
||||
self.base_url = config["homeserver"]
|
||||
self.server_name = config["server_name"]
|
||||
self.user_id = f"@{config['user_id']}:{self.server_name}"
|
||||
self.http = http
|
||||
self.logger = logging.getLogger("appservice")
|
||||
|
||||
# Map events to functions.
|
||||
self.mapping = {
|
||||
"m.room.member": "on_member",
|
||||
"m.room.message": "on_message",
|
||||
"m.room.redaction": "on_redaction",
|
||||
}
|
||||
# Map events to functions.
|
||||
self.mapping = {
|
||||
"m.room.member": "on_member",
|
||||
"m.room.message": "on_message",
|
||||
"m.room.redaction": "on_redaction",
|
||||
}
|
||||
|
||||
# Add route for bottle.
|
||||
self.route(
|
||||
"/transactions/<transaction>",
|
||||
callback=self.receive_event,
|
||||
method="PUT",
|
||||
)
|
||||
# Add route for bottle.
|
||||
self.route(
|
||||
"/transactions/<transaction>",
|
||||
callback=self.receive_event,
|
||||
method="PUT",
|
||||
)
|
||||
|
||||
Cache.cache["m_rooms"] = {}
|
||||
Cache.cache["m_rooms"] = {}
|
||||
|
||||
def handle_event(self, event: dict) -> None:
|
||||
event_type = event.get("type")
|
||||
def handle_event(self, event: dict) -> None:
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type in (
|
||||
"m.room.member",
|
||||
"m.room.message",
|
||||
"m.room.redaction",
|
||||
):
|
||||
obj = matrix.Event(event)
|
||||
else:
|
||||
self.logger.info(f"Unknown event type: {event_type}")
|
||||
return
|
||||
if event_type in (
|
||||
"m.room.member",
|
||||
"m.room.message",
|
||||
"m.room.redaction",
|
||||
):
|
||||
obj = matrix.Event(event)
|
||||
else:
|
||||
self.logger.info(f"Unknown event type: {event_type}")
|
||||
return
|
||||
|
||||
func = getattr(self, self.mapping[event_type], None)
|
||||
func = getattr(self, self.mapping[event_type], None)
|
||||
|
||||
if not func:
|
||||
self.logger.warning(
|
||||
f"Function '{func}' not defined, ignoring event."
|
||||
)
|
||||
return
|
||||
if not func:
|
||||
self.logger.warning(
|
||||
f"Function '{func}' not defined, ignoring event."
|
||||
)
|
||||
return
|
||||
|
||||
# We don't catch exceptions here as the homeserver will re-send us
|
||||
# the event in case of a failure.
|
||||
func(obj)
|
||||
# We don't catch exceptions here as the homeserver will re-send us
|
||||
# the event in case of a failure.
|
||||
func(obj)
|
||||
|
||||
@log_except
|
||||
def receive_event(self, transaction: str) -> dict:
|
||||
"""
|
||||
Verify the homeserver's token and handle events.
|
||||
"""
|
||||
@log_except
|
||||
def receive_event(self, transaction: str) -> dict:
|
||||
"""
|
||||
Verify the homeserver's token and handle events.
|
||||
"""
|
||||
|
||||
hs_token = bottle.request.query.getone("access_token")
|
||||
hs_token = bottle.request.query.getone("access_token")
|
||||
|
||||
if not hs_token:
|
||||
bottle.response.status = 401
|
||||
return {"errcode": "APPSERVICE_UNAUTHORIZED"}
|
||||
if not hs_token:
|
||||
bottle.response.status = 401
|
||||
return {"errcode": "APPSERVICE_UNAUTHORIZED"}
|
||||
|
||||
if hs_token != self.hs_token:
|
||||
bottle.response.status = 403
|
||||
return {"errcode": "APPSERVICE_FORBIDDEN"}
|
||||
if hs_token != self.hs_token:
|
||||
bottle.response.status = 403
|
||||
return {"errcode": "APPSERVICE_FORBIDDEN"}
|
||||
|
||||
events = bottle.request.json.get("events")
|
||||
events = bottle.request.json.get("events")
|
||||
|
||||
for event in events:
|
||||
self.handle_event(event)
|
||||
for event in events:
|
||||
self.handle_event(event)
|
||||
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def mxc_url(self, mxc: str) -> str:
|
||||
try:
|
||||
homeserver, media_id = mxc.replace("mxc://", "").split("/")
|
||||
except ValueError:
|
||||
return ""
|
||||
def mxc_url(self, mxc: str) -> str:
|
||||
try:
|
||||
homeserver, media_id = mxc.replace("mxc://", "").split("/")
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
return (
|
||||
f"https://{self.server_name}/_matrix/media/r0/download/"
|
||||
f"{homeserver}/{media_id}"
|
||||
)
|
||||
return (
|
||||
f"https://{self.server_name}/_matrix/media/r0/download/"
|
||||
f"{homeserver}/{media_id}"
|
||||
)
|
||||
|
||||
def join_room(self, room_id: str, mxid: str = "") -> None:
|
||||
self.send(
|
||||
"POST",
|
||||
f"/join/{room_id}",
|
||||
params={"user_id": mxid} if mxid else {},
|
||||
)
|
||||
def join_room(self, room_id: str, mxid: str = "") -> None:
|
||||
self.send(
|
||||
"POST",
|
||||
f"/join/{room_id}",
|
||||
params={"user_id": mxid} if mxid else {},
|
||||
)
|
||||
|
||||
def redact(self, event_id: str, room_id: str, mxid: str = "") -> None:
|
||||
self.send(
|
||||
"PUT",
|
||||
f"/rooms/{room_id}/redact/{event_id}/{uuid.uuid4()}",
|
||||
params={"user_id": mxid} if mxid else {},
|
||||
)
|
||||
def redact(self, event_id: str, room_id: str, mxid: str = "") -> None:
|
||||
self.send(
|
||||
"PUT",
|
||||
f"/rooms/{room_id}/redact/{event_id}/{uuid.uuid4()}",
|
||||
params={"user_id": mxid} if mxid else {},
|
||||
)
|
||||
|
||||
def get_room_id(self, alias: str) -> str:
|
||||
with Cache.lock:
|
||||
room = Cache.cache["m_rooms"].get(alias)
|
||||
if room:
|
||||
return room
|
||||
def get_room_id(self, alias: str) -> str:
|
||||
with Cache.lock:
|
||||
room = Cache.cache["m_rooms"].get(alias)
|
||||
if room:
|
||||
return room
|
||||
|
||||
resp = self.send("GET", f"/directory/room/{urllib.parse.quote(alias)}")
|
||||
resp = self.send("GET", f"/directory/room/{urllib.parse.quote(alias)}")
|
||||
|
||||
room_id = resp["room_id"]
|
||||
room_id = resp["room_id"]
|
||||
|
||||
with Cache.lock:
|
||||
Cache.cache["m_rooms"][alias] = room_id
|
||||
with Cache.lock:
|
||||
Cache.cache["m_rooms"][alias] = room_id
|
||||
|
||||
return room_id
|
||||
return room_id
|
||||
|
||||
def get_event(self, event_id: str, room_id: str) -> matrix.Event:
|
||||
resp = self.send("GET", f"/rooms/{room_id}/event/{event_id}")
|
||||
def get_event(self, event_id: str, room_id: str) -> matrix.Event:
|
||||
resp = self.send("GET", f"/rooms/{room_id}/event/{event_id}")
|
||||
|
||||
return matrix.Event(resp)
|
||||
return matrix.Event(resp)
|
||||
|
||||
def upload(self, url: str) -> str:
|
||||
"""
|
||||
Upload a file to the homeserver and get the MXC url.
|
||||
"""
|
||||
def upload(self, url: str) -> str:
|
||||
"""
|
||||
Upload a file to the homeserver and get the MXC url.
|
||||
"""
|
||||
|
||||
resp = self.http.request("GET", url)
|
||||
resp = self.http.request("GET", url)
|
||||
|
||||
resp = self.send(
|
||||
"POST",
|
||||
content=resp.data,
|
||||
content_type=resp.headers.get("Content-Type"),
|
||||
params={"filename": f"{uuid.uuid4()}"},
|
||||
endpoint="/_matrix/media/r0/upload",
|
||||
)
|
||||
resp = self.send(
|
||||
"POST",
|
||||
content=resp.data,
|
||||
content_type=resp.headers.get("Content-Type"),
|
||||
params={"filename": f"{uuid.uuid4()}"},
|
||||
endpoint="/_matrix/media/r0/upload",
|
||||
)
|
||||
|
||||
return resp["content_uri"]
|
||||
return resp["content_uri"]
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
room_id: str,
|
||||
content: dict,
|
||||
mxid: str = "",
|
||||
) -> str:
|
||||
resp = self.send(
|
||||
"PUT",
|
||||
f"/rooms/{room_id}/send/m.room.message/{uuid.uuid4()}",
|
||||
content,
|
||||
{"user_id": mxid} if mxid else {},
|
||||
)
|
||||
def send_message(
|
||||
self,
|
||||
room_id: str,
|
||||
content: dict,
|
||||
mxid: str = "",
|
||||
) -> str:
|
||||
resp = self.send(
|
||||
"PUT",
|
||||
f"/rooms/{room_id}/send/m.room.message/{uuid.uuid4()}",
|
||||
content,
|
||||
{"user_id": mxid} if mxid else {},
|
||||
)
|
||||
|
||||
return resp["event_id"]
|
||||
return resp["event_id"]
|
||||
|
||||
def send_typing(
|
||||
self, room_id: str, mxid: str = "", timeout: int = 8000
|
||||
) -> None:
|
||||
self.send(
|
||||
"PUT",
|
||||
f"/rooms/{room_id}/typing/{mxid}",
|
||||
{"typing": True, "timeout": timeout},
|
||||
{"user_id": mxid} if mxid else {},
|
||||
)
|
||||
def send_typing(
|
||||
self, room_id: str, mxid: str = "", timeout: int = 8000
|
||||
) -> None:
|
||||
self.send(
|
||||
"PUT",
|
||||
f"/rooms/{room_id}/typing/{mxid}",
|
||||
{"typing": True, "timeout": timeout},
|
||||
{"user_id": mxid} if mxid else {},
|
||||
)
|
||||
|
||||
def send_invite(self, room_id: str, mxid: str) -> None:
|
||||
self.send("POST", f"/rooms/{room_id}/invite", {"user_id": mxid})
|
||||
def send_invite(self, room_id: str, mxid: str) -> None:
|
||||
self.send("POST", f"/rooms/{room_id}/invite", {"user_id": mxid})
|
||||
|
||||
@request
|
||||
def send(
|
||||
self,
|
||||
method: str,
|
||||
path: str = "",
|
||||
content: Union[bytes, dict] = {},
|
||||
params: dict = {},
|
||||
content_type: str = "application/json",
|
||||
endpoint: str = "/_matrix/client/r0",
|
||||
) -> dict:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.as_token}",
|
||||
"Content-Type": content_type,
|
||||
}
|
||||
payload = json.dumps(content) if isinstance(content, dict) else content
|
||||
endpoint = (
|
||||
f"{self.base_url}{endpoint}{path}?"
|
||||
f"{urllib.parse.urlencode(params)}"
|
||||
)
|
||||
@request
|
||||
def send(
|
||||
self,
|
||||
method: str,
|
||||
path: str = "",
|
||||
content: Union[bytes, dict] = {},
|
||||
params: dict = {},
|
||||
content_type: str = "application/json",
|
||||
endpoint: str = "/_matrix/client/r0",
|
||||
) -> dict:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.as_token}",
|
||||
"Content-Type": content_type,
|
||||
}
|
||||
payload = json.dumps(content) if isinstance(content, dict) else content
|
||||
endpoint = (
|
||||
f"{self.base_url}{endpoint}{path}?"
|
||||
f"{urllib.parse.urlencode(params)}"
|
||||
)
|
||||
|
||||
return self.http.request(
|
||||
method, endpoint, body=payload, headers=headers
|
||||
)
|
||||
return self.http.request(
|
||||
method, endpoint, body=payload, headers=headers
|
||||
)
|
||||
|
|
|
@ -2,5 +2,5 @@ import threading
|
|||
|
||||
|
||||
class Cache:
|
||||
cache = {}
|
||||
lock = threading.Lock()
|
||||
cache = {}
|
||||
lock = threading.Lock()
|
||||
|
|
|
@ -5,116 +5,116 @@ from typing import List
|
|||
|
||||
|
||||
class DataBase:
|
||||
def __init__(self, db_file) -> None:
|
||||
self.create(db_file)
|
||||
def __init__(self, db_file) -> None:
|
||||
self.create(db_file)
|
||||
|
||||
# The database is accessed via multiple threads.
|
||||
self.lock = threading.Lock()
|
||||
# The database is accessed via multiple threads.
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def create(self, db_file) -> None:
|
||||
"""
|
||||
Create a database with the relevant tables if it doesn't already exist.
|
||||
"""
|
||||
def create(self, db_file) -> None:
|
||||
"""
|
||||
Create a database with the relevant tables if it doesn't already exist.
|
||||
"""
|
||||
|
||||
exists = os.path.exists(db_file)
|
||||
exists = os.path.exists(db_file)
|
||||
|
||||
self.conn = sqlite3.connect(db_file, check_same_thread=False)
|
||||
self.conn.row_factory = self.dict_factory
|
||||
self.conn = sqlite3.connect(db_file, check_same_thread=False)
|
||||
self.conn.row_factory = self.dict_factory
|
||||
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur = self.conn.cursor()
|
||||
|
||||
if exists:
|
||||
return
|
||||
if exists:
|
||||
return
|
||||
|
||||
self.cur.execute(
|
||||
"CREATE TABLE bridge(room_id TEXT PRIMARY KEY, channel_id TEXT);"
|
||||
)
|
||||
self.cur.execute(
|
||||
"CREATE TABLE bridge(room_id TEXT PRIMARY KEY, channel_id TEXT);"
|
||||
)
|
||||
|
||||
self.cur.execute(
|
||||
"CREATE TABLE users(mxid TEXT PRIMARY KEY, "
|
||||
"avatar_url TEXT, username TEXT);"
|
||||
)
|
||||
self.cur.execute(
|
||||
"CREATE TABLE users(mxid TEXT PRIMARY KEY, "
|
||||
"avatar_url TEXT, username TEXT);"
|
||||
)
|
||||
|
||||
self.conn.commit()
|
||||
self.conn.commit()
|
||||
|
||||
def dict_factory(self, cursor, row):
|
||||
"""
|
||||
https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.row_factory
|
||||
"""
|
||||
def dict_factory(self, cursor, row):
|
||||
"""
|
||||
https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.row_factory
|
||||
"""
|
||||
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
d[col[0]] = row[idx]
|
||||
return d
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
d[col[0]] = row[idx]
|
||||
return d
|
||||
|
||||
def add_room(self, room_id: str, channel_id: str) -> None:
|
||||
"""
|
||||
Add a bridged room to the database.
|
||||
"""
|
||||
def add_room(self, room_id: str, channel_id: str) -> None:
|
||||
"""
|
||||
Add a bridged room to the database.
|
||||
"""
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"INSERT INTO bridge (room_id, channel_id) VALUES (?, ?)",
|
||||
[room_id, channel_id],
|
||||
)
|
||||
self.conn.commit()
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"INSERT INTO bridge (room_id, channel_id) VALUES (?, ?)",
|
||||
[room_id, channel_id],
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def add_user(self, mxid: str) -> None:
|
||||
with self.lock:
|
||||
self.cur.execute("INSERT INTO users (mxid) VALUES (?)", [mxid])
|
||||
self.conn.commit()
|
||||
def add_user(self, mxid: str) -> None:
|
||||
with self.lock:
|
||||
self.cur.execute("INSERT INTO users (mxid) VALUES (?)", [mxid])
|
||||
self.conn.commit()
|
||||
|
||||
def add_avatar(self, avatar_url: str, mxid: str) -> None:
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"UPDATE users SET avatar_url = (?) WHERE mxid = (?)",
|
||||
[avatar_url, mxid],
|
||||
)
|
||||
self.conn.commit()
|
||||
def add_avatar(self, avatar_url: str, mxid: str) -> None:
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"UPDATE users SET avatar_url = (?) WHERE mxid = (?)",
|
||||
[avatar_url, mxid],
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def add_username(self, username: str, mxid: str) -> None:
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"UPDATE users SET username = (?) WHERE mxid = (?)",
|
||||
[username, mxid],
|
||||
)
|
||||
self.conn.commit()
|
||||
def add_username(self, username: str, mxid: str) -> None:
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"UPDATE users SET username = (?) WHERE mxid = (?)",
|
||||
[username, mxid],
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_channel(self, room_id: str) -> str:
|
||||
"""
|
||||
Get the corresponding channel ID for a given room ID.
|
||||
"""
|
||||
def get_channel(self, room_id: str) -> str:
|
||||
"""
|
||||
Get the corresponding channel ID for a given room ID.
|
||||
"""
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"SELECT channel_id FROM bridge WHERE room_id = ?", [room_id]
|
||||
)
|
||||
with self.lock:
|
||||
self.cur.execute(
|
||||
"SELECT channel_id FROM bridge WHERE room_id = ?", [room_id]
|
||||
)
|
||||
|
||||
room = self.cur.fetchone()
|
||||
room = self.cur.fetchone()
|
||||
|
||||
# Return an empty string if the channel is not bridged.
|
||||
return "" if not room else room["channel_id"]
|
||||
# Return an empty string if the channel is not bridged.
|
||||
return "" if not room else room["channel_id"]
|
||||
|
||||
def list_channels(self) -> List[str]:
|
||||
"""
|
||||
Get a list of all the bridged channels.
|
||||
"""
|
||||
def list_channels(self) -> List[str]:
|
||||
"""
|
||||
Get a list of all the bridged channels.
|
||||
"""
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute("SELECT channel_id FROM bridge")
|
||||
with self.lock:
|
||||
self.cur.execute("SELECT channel_id FROM bridge")
|
||||
|
||||
channels = self.cur.fetchall()
|
||||
channels = self.cur.fetchall()
|
||||
|
||||
return [channel["channel_id"] for channel in channels]
|
||||
return [channel["channel_id"] for channel in channels]
|
||||
|
||||
def fetch_user(self, mxid: str) -> dict:
|
||||
"""
|
||||
Fetch the profile for a bridged user.
|
||||
"""
|
||||
def fetch_user(self, mxid: str) -> dict:
|
||||
"""
|
||||
Fetch the profile for a bridged user.
|
||||
"""
|
||||
|
||||
with self.lock:
|
||||
self.cur.execute("SELECT * FROM users where mxid = ?", [mxid])
|
||||
with self.lock:
|
||||
self.cur.execute("SELECT * FROM users where mxid = ?", [mxid])
|
||||
|
||||
user = self.cur.fetchone()
|
||||
user = self.cur.fetchone()
|
||||
|
||||
return {} if not user else user
|
||||
return {} if not user else user
|
||||
|
|
|
@ -1,218 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from misc import dict_cls
|
||||
|
||||
CDN_URL = "https://cdn.discordapp.com"
|
||||
MESSAGE_LIMIT = 2000
|
||||
|
||||
|
||||
def bitmask(bit: int) -> int:
|
||||
return 1 << bit
|
||||
|
||||
|
||||
@dataclass
|
||||
class Channel:
|
||||
id: str
|
||||
type: str
|
||||
guild_id: str = ""
|
||||
name: str = ""
|
||||
topic: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Emote:
|
||||
animated: bool
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageReference:
|
||||
message_id: str
|
||||
channel_id: str
|
||||
guild_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Sticker:
|
||||
name: str
|
||||
id: str
|
||||
format_type: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Typing:
|
||||
user_id: str
|
||||
channel_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Webhook:
|
||||
id: str
|
||||
token: str
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self, user: dict) -> None:
|
||||
self.discriminator = user["discriminator"]
|
||||
self.id = user["id"]
|
||||
self.mention = f"<@{self.id}>"
|
||||
self.username = user["username"]
|
||||
|
||||
avatar = user["avatar"]
|
||||
|
||||
if not avatar:
|
||||
# https://discord.com/developers/docs/reference#image-formatting
|
||||
self.avatar_url = (
|
||||
f"{CDN_URL}/embed/avatars/{int(self.discriminator) % 5}.png"
|
||||
)
|
||||
else:
|
||||
ext = "gif" if avatar.startswith("a_") else "png"
|
||||
self.avatar_url = f"{CDN_URL}/avatars/{self.id}/{avatar}.{ext}"
|
||||
|
||||
|
||||
class Guild:
|
||||
def __init__(self, guild: dict) -> None:
|
||||
self.guild_id = guild["id"]
|
||||
self.channels = [dict_cls(c, Channel) for c in guild["channels"]]
|
||||
self.emojis = [dict_cls(e, Emote) for e in guild["emojis"]]
|
||||
members = [member["user"] for member in guild["members"]]
|
||||
self.members = [User(m) for m in members]
|
||||
|
||||
|
||||
class GuildEmojisUpdate:
|
||||
def __init__(self, update: dict) -> None:
|
||||
self.guild_id = update["guild_id"]
|
||||
self.emojis = [dict_cls(e, Emote) for e in update["emojis"]]
|
||||
|
||||
|
||||
class GuildMembersChunk:
|
||||
def __init__(self, chunk: dict) -> None:
|
||||
self.chunk_index = chunk["chunk_index"]
|
||||
self.chunk_count = chunk["chunk_count"]
|
||||
self.guild_id = chunk["guild_id"]
|
||||
self.members = [User(m) for m in chunk["members"]]
|
||||
|
||||
|
||||
class GuildMemberUpdate:
|
||||
def __init__(self, update: dict) -> None:
|
||||
self.guild_id = update["guild_id"]
|
||||
self.user = User(update["user"])
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(self, message: dict) -> None:
|
||||
self.attachments = message.get("attachments", [])
|
||||
self.channel_id = message["channel_id"]
|
||||
self.content = message.get("content", "")
|
||||
self.id = message["id"]
|
||||
self.guild_id = message.get(
|
||||
"guild_id", ""
|
||||
) # Responses for sending webhook messages don't have guild_id
|
||||
self.webhook_id = message.get("webhook_id", "")
|
||||
self.application_id = message.get("application_id", "")
|
||||
|
||||
self.mentions = [
|
||||
User(mention) for mention in message.get("mentions", [])
|
||||
]
|
||||
|
||||
ref = message.get("referenced_message")
|
||||
|
||||
self.referenced_message = Message(ref) if ref else None
|
||||
|
||||
author = message.get("author")
|
||||
|
||||
self.author = User(author) if author else None
|
||||
|
||||
self.stickers = [
|
||||
dict_cls(sticker, Sticker)
|
||||
for sticker in message.get("sticker_items", [])
|
||||
]
|
||||
|
||||
|
||||
class ChannelType:
|
||||
GUILD_TEXT = 0
|
||||
DM = 1
|
||||
GUILD_VOICE = 2
|
||||
GROUP_DM = 3
|
||||
GUILD_CATEGORY = 4
|
||||
GUILD_NEWS = 5
|
||||
GUILD_STORE = 6
|
||||
|
||||
|
||||
class InteractionResponseType:
|
||||
PONG = 0
|
||||
ACKNOWLEDGE = 1
|
||||
CHANNEL_MESSAGE = 2
|
||||
CHANNEL_MESSAGE_WITH_SOURCE = 4
|
||||
ACKNOWLEDGE_WITH_SOURCE = 5
|
||||
|
||||
|
||||
class GatewayIntents:
|
||||
GUILDS = bitmask(0)
|
||||
GUILD_MEMBERS = bitmask(1)
|
||||
GUILD_BANS = bitmask(2)
|
||||
GUILD_EMOJIS = bitmask(3)
|
||||
GUILD_INTEGRATIONS = bitmask(4)
|
||||
GUILD_WEBHOOKS = bitmask(5)
|
||||
GUILD_INVITES = bitmask(6)
|
||||
GUILD_VOICE_STATES = bitmask(7)
|
||||
GUILD_PRESENCES = bitmask(8)
|
||||
GUILD_MESSAGES = bitmask(9)
|
||||
GUILD_MESSAGE_REACTIONS = bitmask(10)
|
||||
GUILD_MESSAGE_TYPING = bitmask(11)
|
||||
DIRECT_MESSAGES = bitmask(12)
|
||||
DIRECT_MESSAGE_REACTIONS = bitmask(13)
|
||||
DIRECT_MESSAGE_TYPING = bitmask(14)
|
||||
|
||||
|
||||
class GatewayOpCodes:
|
||||
DISPATCH = 0
|
||||
HEARTBEAT = 1
|
||||
IDENTIFY = 2
|
||||
PRESENCE_UPDATE = 3
|
||||
VOICE_STATE_UPDATE = 4
|
||||
RESUME = 6
|
||||
RECONNECT = 7
|
||||
REQUEST_GUILD_MEMBERS = 8
|
||||
INVALID_SESSION = 9
|
||||
HELLO = 10
|
||||
HEARTBEAT_ACK = 11
|
||||
|
||||
|
||||
class Payloads:
|
||||
def __init__(self, token: str) -> None:
|
||||
self.seq = self.session = None
|
||||
self.token = token
|
||||
|
||||
def HEARTBEAT(self) -> dict:
|
||||
return {"op": GatewayOpCodes.HEARTBEAT, "d": self.seq}
|
||||
|
||||
def IDENTIFY(self) -> dict:
|
||||
return {
|
||||
"op": GatewayOpCodes.IDENTIFY,
|
||||
"d": {
|
||||
"token": self.token,
|
||||
"intents": GatewayIntents.GUILDS
|
||||
| GatewayIntents.GUILD_EMOJIS
|
||||
| GatewayIntents.GUILD_MEMBERS
|
||||
| GatewayIntents.GUILD_MESSAGES
|
||||
| GatewayIntents.GUILD_MESSAGE_TYPING
|
||||
| GatewayIntents.GUILD_PRESENCES,
|
||||
"properties": {
|
||||
"$os": "discord",
|
||||
"$browser": "Discord Client",
|
||||
"$device": "discord",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def RESUME(self) -> dict:
|
||||
return {
|
||||
"op": GatewayOpCodes.RESUME,
|
||||
"d": {
|
||||
"token": self.token,
|
||||
"session_id": self.session,
|
||||
"seq": self.seq,
|
||||
},
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
class RequestError(Exception):
|
||||
def __init__(self, status: int, *args):
|
||||
super().__init__(*args)
|
||||
def __init__(self, status: int, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
self.status = status
|
||||
self.status = status
|
||||
|
|
|
@ -12,249 +12,249 @@ from misc import dict_cls, log_except, request
|
|||
|
||||
|
||||
class Gateway:
|
||||
def __init__(self, http: urllib3.PoolManager, token: str):
|
||||
self.http = http
|
||||
self.token = token
|
||||
self.logger = logging.getLogger("discord")
|
||||
self.Payloads = discord.Payloads(self.token)
|
||||
self.websocket = None
|
||||
def __init__(self, http: urllib3.PoolManager, token: str):
|
||||
self.http = http
|
||||
self.token = token
|
||||
self.logger = logging.getLogger("discord")
|
||||
self.Payloads = discord.Payloads(self.token)
|
||||
self.websocket = None
|
||||
|
||||
@log_except
|
||||
async def run(self) -> None:
|
||||
self.heartbeat_task: asyncio.Future = None
|
||||
self.resume = False
|
||||
@log_except
|
||||
async def run(self) -> None:
|
||||
self.heartbeat_task: asyncio.Future = None
|
||||
self.resume = False
|
||||
|
||||
gateway_url = self.get_gateway_url()
|
||||
gateway_url = self.get_gateway_url()
|
||||
|
||||
while True:
|
||||
try:
|
||||
await self.gateway_handler(gateway_url)
|
||||
except (
|
||||
websockets.ConnectionClosedError,
|
||||
websockets.InvalidMessage,
|
||||
):
|
||||
self.logger.exception("Connection lost, reconnecting.")
|
||||
while True:
|
||||
try:
|
||||
await self.gateway_handler(gateway_url)
|
||||
except (
|
||||
websockets.ConnectionClosedError,
|
||||
websockets.InvalidMessage,
|
||||
):
|
||||
self.logger.exception("Connection lost, reconnecting.")
|
||||
|
||||
# Stop sending heartbeats until we reconnect.
|
||||
if self.heartbeat_task and not self.heartbeat_task.cancelled():
|
||||
self.heartbeat_task.cancel()
|
||||
# Stop sending heartbeats until we reconnect.
|
||||
if self.heartbeat_task and not self.heartbeat_task.cancelled():
|
||||
self.heartbeat_task.cancel()
|
||||
|
||||
def get_gateway_url(self) -> str:
|
||||
resp = self.send("GET", "/gateway")
|
||||
def get_gateway_url(self) -> str:
|
||||
resp = self.send("GET", "/gateway")
|
||||
|
||||
return resp["url"]
|
||||
return resp["url"]
|
||||
|
||||
async def heartbeat_handler(self, interval_ms: int) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(interval_ms / 1000)
|
||||
await self.websocket.send(json.dumps(self.Payloads.HEARTBEAT()))
|
||||
async def heartbeat_handler(self, interval_ms: int) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(interval_ms / 1000)
|
||||
await self.websocket.send(json.dumps(self.Payloads.HEARTBEAT()))
|
||||
|
||||
async def handle_resp(self, data: dict) -> None:
|
||||
data_dict = data["d"]
|
||||
async def handle_resp(self, data: dict) -> None:
|
||||
data_dict = data["d"]
|
||||
|
||||
opcode = data["op"]
|
||||
opcode = data["op"]
|
||||
|
||||
seq = data["s"]
|
||||
seq = data["s"]
|
||||
|
||||
if seq:
|
||||
self.Payloads.seq = seq
|
||||
if seq:
|
||||
self.Payloads.seq = seq
|
||||
|
||||
if opcode == discord.GatewayOpCodes.DISPATCH:
|
||||
otype = data["t"]
|
||||
if opcode == discord.GatewayOpCodes.DISPATCH:
|
||||
otype = data["t"]
|
||||
|
||||
if otype == "READY":
|
||||
self.Payloads.session = data_dict["session_id"]
|
||||
if otype == "READY":
|
||||
self.Payloads.session = data_dict["session_id"]
|
||||
|
||||
self.logger.info("READY")
|
||||
else:
|
||||
self.handle_otype(data_dict, otype)
|
||||
elif opcode == discord.GatewayOpCodes.HELLO:
|
||||
heartbeat_interval = data_dict.get("heartbeat_interval")
|
||||
self.logger.info("READY")
|
||||
else:
|
||||
self.handle_otype(data_dict, otype)
|
||||
elif opcode == discord.GatewayOpCodes.HELLO:
|
||||
heartbeat_interval = data_dict.get("heartbeat_interval")
|
||||
|
||||
self.logger.info(f"Heartbeat Interval: {heartbeat_interval}")
|
||||
self.logger.info(f"Heartbeat Interval: {heartbeat_interval}")
|
||||
|
||||
# Send periodic hearbeats to gateway.
|
||||
self.heartbeat_task = asyncio.ensure_future(
|
||||
self.heartbeat_handler(heartbeat_interval)
|
||||
)
|
||||
# Send periodic hearbeats to gateway.
|
||||
self.heartbeat_task = asyncio.ensure_future(
|
||||
self.heartbeat_handler(heartbeat_interval)
|
||||
)
|
||||
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
self.Payloads.RESUME()
|
||||
if self.resume
|
||||
else self.Payloads.IDENTIFY()
|
||||
)
|
||||
)
|
||||
elif opcode == discord.GatewayOpCodes.RECONNECT:
|
||||
self.logger.info("Received RECONNECT.")
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
self.Payloads.RESUME()
|
||||
if self.resume
|
||||
else self.Payloads.IDENTIFY()
|
||||
)
|
||||
)
|
||||
elif opcode == discord.GatewayOpCodes.RECONNECT:
|
||||
self.logger.info("Received RECONNECT.")
|
||||
|
||||
self.resume = True
|
||||
await self.websocket.close()
|
||||
elif opcode == discord.GatewayOpCodes.INVALID_SESSION:
|
||||
self.logger.info("Received INVALID_SESSION.")
|
||||
self.resume = True
|
||||
await self.websocket.close()
|
||||
elif opcode == discord.GatewayOpCodes.INVALID_SESSION:
|
||||
self.logger.info("Received INVALID_SESSION.")
|
||||
|
||||
self.resume = False
|
||||
await self.websocket.close()
|
||||
elif opcode == discord.GatewayOpCodes.HEARTBEAT_ACK:
|
||||
# NOP
|
||||
pass
|
||||
else:
|
||||
self.logger.info(
|
||||
"Unknown OP code: {opcode}\n{json.dumps(data, indent=4)}"
|
||||
)
|
||||
self.resume = False
|
||||
await self.websocket.close()
|
||||
elif opcode == discord.GatewayOpCodes.HEARTBEAT_ACK:
|
||||
# NOP
|
||||
pass
|
||||
else:
|
||||
self.logger.info(
|
||||
"Unknown OP code: {opcode}\n{json.dumps(data, indent=4)}"
|
||||
)
|
||||
|
||||
def handle_otype(self, data: dict, otype: str) -> None:
|
||||
if otype in ("MESSAGE_CREATE", "MESSAGE_UPDATE", "MESSAGE_DELETE"):
|
||||
obj = discord.Message(data)
|
||||
elif otype == "TYPING_START":
|
||||
obj = dict_cls(data, discord.Typing)
|
||||
elif otype == "GUILD_CREATE":
|
||||
obj = discord.Guild(data)
|
||||
elif otype == "GUILD_MEMBER_UPDATE":
|
||||
obj = discord.GuildMemberUpdate(data)
|
||||
elif otype == "GUILD_EMOJIS_UPDATE":
|
||||
obj = discord.GuildEmojisUpdate(data)
|
||||
else:
|
||||
return
|
||||
def handle_otype(self, data: dict, otype: str) -> None:
|
||||
if otype in ("MESSAGE_CREATE", "MESSAGE_UPDATE", "MESSAGE_DELETE"):
|
||||
obj = discord.Message(data)
|
||||
elif otype == "TYPING_START":
|
||||
obj = dict_cls(data, discord.Typing)
|
||||
elif otype == "GUILD_CREATE":
|
||||
obj = discord.Guild(data)
|
||||
elif otype == "GUILD_MEMBER_UPDATE":
|
||||
obj = discord.GuildMemberUpdate(data)
|
||||
elif otype == "GUILD_EMOJIS_UPDATE":
|
||||
obj = discord.GuildEmojisUpdate(data)
|
||||
else:
|
||||
return
|
||||
|
||||
func = getattr(self, f"on_{otype.lower()}", None)
|
||||
func = getattr(self, f"on_{otype.lower()}", None)
|
||||
|
||||
if not func:
|
||||
self.logger.warning(
|
||||
f"Function '{func}' not defined, ignoring message."
|
||||
)
|
||||
return
|
||||
if not func:
|
||||
self.logger.warning(
|
||||
f"Function '{func}' not defined, ignoring message."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
func(obj)
|
||||
except Exception:
|
||||
self.logger.exception(f"Ignoring exception in '{func.__name__}':")
|
||||
try:
|
||||
func(obj)
|
||||
except Exception:
|
||||
self.logger.exception(f"Ignoring exception in '{func.__name__}':")
|
||||
|
||||
async def gateway_handler(self, gateway_url: str) -> None:
|
||||
async with websockets.connect(
|
||||
f"{gateway_url}/?v=8&encoding=json"
|
||||
) as websocket:
|
||||
self.websocket = websocket
|
||||
async def gateway_handler(self, gateway_url: str) -> None:
|
||||
async with websockets.connect(
|
||||
f"{gateway_url}/?v=8&encoding=json"
|
||||
) as websocket:
|
||||
self.websocket = websocket
|
||||
|
||||
async for message in websocket:
|
||||
await self.handle_resp(json.loads(message))
|
||||
async for message in websocket:
|
||||
await self.handle_resp(json.loads(message))
|
||||
|
||||
def get_channel(self, channel_id: str) -> discord.Channel:
|
||||
"""
|
||||
Get the channel for a given channel ID.
|
||||
"""
|
||||
def get_channel(self, channel_id: str) -> discord.Channel:
|
||||
"""
|
||||
Get the channel for a given channel ID.
|
||||
"""
|
||||
|
||||
resp = self.send("GET", f"/channels/{channel_id}")
|
||||
resp = self.send("GET", f"/channels/{channel_id}")
|
||||
|
||||
return dict_cls(resp, discord.Channel)
|
||||
return dict_cls(resp, discord.Channel)
|
||||
|
||||
def get_channels(self, guild_id: str) -> Dict[str, discord.Channel]:
|
||||
"""
|
||||
Get all channels for a given guild ID.
|
||||
"""
|
||||
def get_channels(self, guild_id: str) -> Dict[str, discord.Channel]:
|
||||
"""
|
||||
Get all channels for a given guild ID.
|
||||
"""
|
||||
|
||||
resp = self.send("GET", f"/guilds/{guild_id}/channels")
|
||||
resp = self.send("GET", f"/guilds/{guild_id}/channels")
|
||||
|
||||
return {
|
||||
channel["id"]: dict_cls(channel, discord.Channel)
|
||||
for channel in resp
|
||||
}
|
||||
return {
|
||||
channel["id"]: dict_cls(channel, discord.Channel)
|
||||
for channel in resp
|
||||
}
|
||||
|
||||
def get_emotes(self, guild_id: str) -> List[discord.Emote]:
|
||||
"""
|
||||
Get all the emotes for a given guild.
|
||||
"""
|
||||
def get_emotes(self, guild_id: str) -> List[discord.Emote]:
|
||||
"""
|
||||
Get all the emotes for a given guild.
|
||||
"""
|
||||
|
||||
resp = self.send("GET", f"/guilds/{guild_id}/emojis")
|
||||
resp = self.send("GET", f"/guilds/{guild_id}/emojis")
|
||||
|
||||
return [dict_cls(emote, discord.Emote) for emote in resp]
|
||||
return [dict_cls(emote, discord.Emote) for emote in resp]
|
||||
|
||||
def get_members(self, guild_id: str) -> List[discord.User]:
|
||||
"""
|
||||
Get all the members for a given guild.
|
||||
"""
|
||||
def get_members(self, guild_id: str) -> List[discord.User]:
|
||||
"""
|
||||
Get all the members for a given guild.
|
||||
"""
|
||||
|
||||
resp = self.send(
|
||||
"GET", f"/guilds/{guild_id}/members", params={"limit": 1000}
|
||||
)
|
||||
resp = self.send(
|
||||
"GET", f"/guilds/{guild_id}/members", params={"limit": 1000}
|
||||
)
|
||||
|
||||
return [discord.User(member["user"]) for member in resp]
|
||||
return [discord.User(member["user"]) for member in resp]
|
||||
|
||||
def create_webhook(self, channel_id: str, name: str) -> discord.Webhook:
|
||||
"""
|
||||
Create a webhook with the specified name in a given channel.
|
||||
"""
|
||||
def create_webhook(self, channel_id: str, name: str) -> discord.Webhook:
|
||||
"""
|
||||
Create a webhook with the specified name in a given channel.
|
||||
"""
|
||||
|
||||
resp = self.send(
|
||||
"POST", f"/channels/{channel_id}/webhooks", {"name": name}
|
||||
)
|
||||
resp = self.send(
|
||||
"POST", f"/channels/{channel_id}/webhooks", {"name": name}
|
||||
)
|
||||
|
||||
return dict_cls(resp, discord.Webhook)
|
||||
return dict_cls(resp, discord.Webhook)
|
||||
|
||||
def edit_webhook(
|
||||
self, content: str, message_id: str, webhook: discord.Webhook
|
||||
) -> None:
|
||||
self.send(
|
||||
"PATCH",
|
||||
f"/webhooks/{webhook.id}/{webhook.token}/messages/"
|
||||
f"{message_id}",
|
||||
{"content": content},
|
||||
)
|
||||
def edit_webhook(
|
||||
self, content: str, message_id: str, webhook: discord.Webhook
|
||||
) -> None:
|
||||
self.send(
|
||||
"PATCH",
|
||||
f"/webhooks/{webhook.id}/{webhook.token}/messages/"
|
||||
f"{message_id}",
|
||||
{"content": content},
|
||||
)
|
||||
|
||||
def delete_webhook(
|
||||
self, message_id: str, webhook: discord.Webhook
|
||||
) -> None:
|
||||
self.send(
|
||||
"DELETE",
|
||||
f"/webhooks/{webhook.id}/{webhook.token}/messages/"
|
||||
f"{message_id}",
|
||||
)
|
||||
def delete_webhook(
|
||||
self, message_id: str, webhook: discord.Webhook
|
||||
) -> None:
|
||||
self.send(
|
||||
"DELETE",
|
||||
f"/webhooks/{webhook.id}/{webhook.token}/messages/"
|
||||
f"{message_id}",
|
||||
)
|
||||
|
||||
def send_webhook(
|
||||
self,
|
||||
webhook: discord.Webhook,
|
||||
avatar_url: str,
|
||||
content: str,
|
||||
username: str,
|
||||
) -> discord.Message:
|
||||
payload = {
|
||||
"avatar_url": avatar_url,
|
||||
"content": content,
|
||||
"username": username,
|
||||
# Disable 'everyone' and 'role' mentions.
|
||||
"allowed_mentions": {"parse": ["users"]},
|
||||
}
|
||||
def send_webhook(
|
||||
self,
|
||||
webhook: discord.Webhook,
|
||||
avatar_url: str,
|
||||
content: str,
|
||||
username: str,
|
||||
) -> discord.Message:
|
||||
payload = {
|
||||
"avatar_url": avatar_url,
|
||||
"content": content,
|
||||
"username": username,
|
||||
# Disable 'everyone' and 'role' mentions.
|
||||
"allowed_mentions": {"parse": ["users"]},
|
||||
}
|
||||
|
||||
resp = self.send(
|
||||
"POST",
|
||||
f"/webhooks/{webhook.id}/{webhook.token}",
|
||||
payload,
|
||||
{"wait": True},
|
||||
)
|
||||
resp = self.send(
|
||||
"POST",
|
||||
f"/webhooks/{webhook.id}/{webhook.token}",
|
||||
payload,
|
||||
{"wait": True},
|
||||
)
|
||||
|
||||
return discord.Message(resp)
|
||||
return discord.Message(resp)
|
||||
|
||||
def send_message(self, message: str, channel_id: str) -> None:
|
||||
self.send(
|
||||
"POST", f"/channels/{channel_id}/messages", {"content": message}
|
||||
)
|
||||
def send_message(self, message: str, channel_id: str) -> None:
|
||||
self.send(
|
||||
"POST", f"/channels/{channel_id}/messages", {"content": message}
|
||||
)
|
||||
|
||||
@request
|
||||
def send(
|
||||
self, method: str, path: str, content: dict = {}, params: dict = {}
|
||||
) -> dict:
|
||||
endpoint = (
|
||||
f"https://discord.com/api/v8{path}?"
|
||||
f"{urllib.parse.urlencode(params)}"
|
||||
)
|
||||
headers = {
|
||||
"Authorization": f"Bot {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
@request
|
||||
def send(
|
||||
self, method: str, path: str, content: dict = {}, params: dict = {}
|
||||
) -> dict:
|
||||
endpoint = (
|
||||
f"https://discord.com/api/v8{path}?"
|
||||
f"{urllib.parse.urlencode(params)}"
|
||||
)
|
||||
headers = {
|
||||
"Authorization": f"Bot {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# 'body' being an empty dict breaks "GET" requests.
|
||||
payload = json.dumps(content) if content else None
|
||||
# 'body' being an empty dict breaks "GET" requests.
|
||||
payload = json.dumps(content) if content else None
|
||||
|
||||
return self.http.request(
|
||||
method, endpoint, body=payload, headers=headers
|
||||
)
|
||||
return self.http.request(
|
||||
method, endpoint, body=payload, headers=headers
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,26 +3,26 @@ from dataclasses import dataclass
|
|||
|
||||
@dataclass
|
||||
class User:
|
||||
avatar_url: str = ""
|
||||
display_name: str = ""
|
||||
avatar_url: str = ""
|
||||
display_name: str = ""
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, event: dict):
|
||||
content = event.get("content", {})
|
||||
def __init__(self, event: dict):
|
||||
content = event.get("content", {})
|
||||
|
||||
self.attachment = content.get("url")
|
||||
self.body = content.get("body", "").strip()
|
||||
self.formatted_body = content.get("formatted_body", "")
|
||||
self.id = event["event_id"]
|
||||
self.is_direct = content.get("is_direct", False)
|
||||
self.redacts = event.get("redacts", "")
|
||||
self.room_id = event["room_id"]
|
||||
self.sender = event["sender"]
|
||||
self.state_key = event.get("state_key", "")
|
||||
self.attachment = content.get("url")
|
||||
self.body = content.get("body", "").strip()
|
||||
self.formatted_body = content.get("formatted_body", "")
|
||||
self.id = event["event_id"]
|
||||
self.is_direct = content.get("is_direct", False)
|
||||
self.redacts = event.get("redacts", "")
|
||||
self.room_id = event["room_id"]
|
||||
self.sender = event["sender"]
|
||||
self.state_key = event.get("state_key", "")
|
||||
|
||||
rel = content.get("m.relates_to", {})
|
||||
rel = content.get("m.relates_to", {})
|
||||
|
||||
self.relates_to = rel.get("event_id")
|
||||
self.reltype = rel.get("rel_type")
|
||||
self.new_body = content.get("m.new_content", {}).get("body", "")
|
||||
self.relates_to = rel.get("event_id")
|
||||
self.reltype = rel.get("rel_type")
|
||||
self.new_body = content.get("m.new_content", {}).get("body", "")
|
||||
|
|
|
@ -8,77 +8,77 @@ from errors import RequestError
|
|||
|
||||
|
||||
def dict_cls(d: dict, cls: Any) -> Any:
|
||||
"""
|
||||
Create a dataclass from a dictionary.
|
||||
"""
|
||||
"""
|
||||
Create a dataclass from a dictionary.
|
||||
"""
|
||||
|
||||
field_names = set(f.name for f in fields(cls))
|
||||
filtered_dict = {k: v for k, v in d.items() if k in field_names}
|
||||
field_names = set(f.name for f in fields(cls))
|
||||
filtered_dict = {k: v for k, v in d.items() if k in field_names}
|
||||
|
||||
return cls(**filtered_dict)
|
||||
return cls(**filtered_dict)
|
||||
|
||||
|
||||
def log_except(fn):
|
||||
"""
|
||||
Log unhandled exceptions to a logger instead of `stderr`.
|
||||
"""
|
||||
"""
|
||||
Log unhandled exceptions to a logger instead of `stderr`.
|
||||
"""
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return fn(self, *args, **kwargs)
|
||||
except Exception:
|
||||
self.logger.exception(f"Exception in '{fn.__name__}':")
|
||||
raise
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return fn(self, *args, **kwargs)
|
||||
except Exception:
|
||||
self.logger.exception(f"Exception in '{fn.__name__}':")
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
||||
|
||||
def request(fn):
|
||||
"""
|
||||
Either return json data or raise a `RequestError` if the request was
|
||||
unsuccessful.
|
||||
"""
|
||||
"""
|
||||
Either return json data or raise a `RequestError` if the request was
|
||||
unsuccessful.
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
resp = fn(*args, **kwargs)
|
||||
except urllib3.exceptions.HTTPError as e:
|
||||
raise RequestError(None, f"Failed to connect: {e}") from None
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
resp = fn(*args, **kwargs)
|
||||
except urllib3.exceptions.HTTPError as e:
|
||||
raise RequestError(None, f"Failed to connect: {e}") from None
|
||||
|
||||
if resp.status < 200 or resp.status >= 300:
|
||||
raise RequestError(
|
||||
resp.status,
|
||||
f"Failed to get response from '{resp.geturl()}':\n{resp.data}",
|
||||
)
|
||||
if resp.status < 200 or resp.status >= 300:
|
||||
raise RequestError(
|
||||
resp.status,
|
||||
f"Failed to get response from '{resp.geturl()}':\n{resp.data}",
|
||||
)
|
||||
|
||||
return {} if resp.status == 204 else json.loads(resp.data)
|
||||
return {} if resp.status == 204 else json.loads(resp.data)
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
||||
|
||||
def except_deleted(fn):
|
||||
"""
|
||||
Ignore the `RequestError` on 404s, the content might have been removed.
|
||||
"""
|
||||
"""
|
||||
Ignore the `RequestError` on 404s, the content might have been removed.
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except RequestError as e:
|
||||
if e.status != 404:
|
||||
raise
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except RequestError as e:
|
||||
if e.status != 404:
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
return wrapper
|
||||
|
||||
|
||||
def hash_str(string: str) -> int:
|
||||
"""
|
||||
Create the hash for a string
|
||||
"""
|
||||
"""
|
||||
Create the hash for a string
|
||||
"""
|
||||
|
||||
hash = 5381
|
||||
hash = 5381
|
||||
|
||||
for ch in string:
|
||||
hash = ((hash << 5) + hash) + ord(ch)
|
||||
for ch in string:
|
||||
hash = ((hash << 5) + hash) + ord(ch)
|
||||
|
||||
return hash & 0xFFFFFFFF
|
||||
return hash & 0xFFFFFFFF
|
||||
|
|
Loading…
Reference in a new issue