Add rooms & pending invites to client page, with the options to leave a room and accept, ignore or reject an invite
This commit is contained in:
parent
3e8e034a5a
commit
007fcb1c02
12 changed files with 482 additions and 20 deletions
29
alembic/versions/fcb4ea0fce29_create_invite_table.py
Normal file
29
alembic/versions/fcb4ea0fce29_create_invite_table.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
"""create invite table
|
||||||
|
|
||||||
|
Revision ID: fcb4ea0fce29
|
||||||
|
Revises: 90aa88820eab
|
||||||
|
Create Date: 2022-01-18 02:16:53.954662
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fcb4ea0fce29'
|
||||||
|
down_revision = '90aa88820eab'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table('invite',
|
||||||
|
sa.Column('client', sa.String(255), sa.ForeignKey("client.id", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
|
||||||
|
sa.Column('room', sa.String(255), nullable=False, primary_key=True),
|
||||||
|
sa.Column('date', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('inviter', sa.String(255), nullable=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('invite')
|
|
@ -13,21 +13,22 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from typing import Dict, Iterable, Optional, Set, Callable, Any, Awaitable, Union, TYPE_CHECKING
|
from typing import Dict, Iterable, List, Optional, Set, Callable, Any, Awaitable, Union, TYPE_CHECKING
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
from mautrix.errors import MatrixInvalidToken
|
from mautrix.errors import MatrixInvalidToken
|
||||||
from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStateEvent, Membership,
|
from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStateEvent, Membership,
|
||||||
StateEvent, EventType, Filter, RoomFilter, RoomEventFilter, EventFilter,
|
StateEvent, EventType, Filter, RoomFilter, RoomEventFilter, EventFilter,
|
||||||
PresenceState, StateFilter, DeviceID)
|
PresenceState, StateFilter, DeviceID, RoomID)
|
||||||
from mautrix.client import InternalEventType
|
from mautrix.client import InternalEventType
|
||||||
from mautrix.client.state_store.sqlalchemy import SQLStateStore as BaseSQLStateStore
|
from mautrix.client.state_store.sqlalchemy import SQLStateStore as BaseSQLStateStore
|
||||||
|
|
||||||
from .lib.store_proxy import SyncStoreProxy
|
from .lib.store_proxy import SyncStoreProxy
|
||||||
from .db import DBClient
|
from .db import DBClient, DBInvite
|
||||||
from .matrix import MaubotMatrixClient
|
from .matrix import MaubotMatrixClient
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -65,9 +66,11 @@ class Client:
|
||||||
|
|
||||||
remote_displayname: Optional[str]
|
remote_displayname: Optional[str]
|
||||||
remote_avatar_url: Optional[ContentURI]
|
remote_avatar_url: Optional[ContentURI]
|
||||||
|
remote_rooms: Optional[List[RoomID]]
|
||||||
|
|
||||||
def __init__(self, db_instance: DBClient) -> None:
|
def __init__(self, db_instance: DBClient) -> None:
|
||||||
self.db_instance = db_instance
|
self.db_instance = db_instance
|
||||||
|
self.db_invites = DBInvite.get(self.id)
|
||||||
self.cache[self.id] = self
|
self.cache[self.id] = self
|
||||||
self.log = log.getChild(self.id)
|
self.log = log.getChild(self.id)
|
||||||
self.references = set()
|
self.references = set()
|
||||||
|
@ -75,6 +78,7 @@ class Client:
|
||||||
self.sync_ok = True
|
self.sync_ok = True
|
||||||
self.remote_displayname = None
|
self.remote_displayname = None
|
||||||
self.remote_avatar_url = None
|
self.remote_avatar_url = None
|
||||||
|
self.remote_rooms = None
|
||||||
self.client = MaubotMatrixClient(mxid=self.id, base_url=self.homeserver,
|
self.client = MaubotMatrixClient(mxid=self.id, base_url=self.homeserver,
|
||||||
token=self.access_token, client_session=self.http_client,
|
token=self.access_token, client_session=self.http_client,
|
||||||
log=self.log, loop=self.loop, device_id=self.device_id,
|
log=self.log, loop=self.loop, device_id=self.device_id,
|
||||||
|
@ -88,8 +92,7 @@ class Client:
|
||||||
self.client.ignore_initial_sync = True
|
self.client.ignore_initial_sync = True
|
||||||
self.client.ignore_first_sync = True
|
self.client.ignore_first_sync = True
|
||||||
self.client.presence = PresenceState.ONLINE if self.online else PresenceState.OFFLINE
|
self.client.presence = PresenceState.ONLINE if self.online else PresenceState.OFFLINE
|
||||||
if self.autojoin:
|
self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
||||||
self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
|
||||||
self.client.add_event_handler(EventType.ROOM_TOMBSTONE, self._handle_tombstone)
|
self.client.add_event_handler(EventType.ROOM_TOMBSTONE, self._handle_tombstone)
|
||||||
self.client.add_event_handler(InternalEventType.SYNC_ERRORED, self._set_sync_ok(False))
|
self.client.add_event_handler(InternalEventType.SYNC_ERRORED, self._set_sync_ok(False))
|
||||||
self.client.add_event_handler(InternalEventType.SYNC_SUCCESSFUL, self._set_sync_ok(True))
|
self.client.add_event_handler(InternalEventType.SYNC_SUCCESSFUL, self._set_sync_ok(True))
|
||||||
|
@ -253,6 +256,13 @@ class Client:
|
||||||
"avatar_url": self.avatar_url,
|
"avatar_url": self.avatar_url,
|
||||||
"remote_displayname": self.remote_displayname,
|
"remote_displayname": self.remote_displayname,
|
||||||
"remote_avatar_url": self.remote_avatar_url,
|
"remote_avatar_url": self.remote_avatar_url,
|
||||||
|
"invites": [{
|
||||||
|
"client": i.client,
|
||||||
|
"room": i.room,
|
||||||
|
"date": i.date.timestamp(),
|
||||||
|
"inviter": i.inviter
|
||||||
|
} for i in self.invites],
|
||||||
|
"rooms": self.remote_rooms,
|
||||||
"instances": [instance.to_dict() for instance in self.references],
|
"instances": [instance.to_dict() for instance in self.references],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,11 +285,23 @@ class Client:
|
||||||
self.log.info(f"{evt.room_id} tombstoned with no replacement, ignoring")
|
self.log.info(f"{evt.room_id} tombstoned with no replacement, ignoring")
|
||||||
return
|
return
|
||||||
_, server = self.client.parse_user_id(evt.sender)
|
_, server = self.client.parse_user_id(evt.sender)
|
||||||
|
DBInvite.update_tombstone(user.id, evt.room_id, evt.content.replacement_room)
|
||||||
await self.client.join_room(evt.content.replacement_room, servers=[server])
|
await self.client.join_room(evt.content.replacement_room, servers=[server])
|
||||||
|
|
||||||
async def _handle_invite(self, evt: StrippedStateEvent) -> None:
|
async def _handle_invite(self, evt: StrippedStateEvent) -> None:
|
||||||
if evt.state_key == self.id and evt.content.membership == Membership.INVITE:
|
if evt.state_key != self.id or evt.content.membership != Membership.INVITE:
|
||||||
|
return
|
||||||
|
if self.autojoin:
|
||||||
await self.client.join_room(evt.room_id)
|
await self.client.join_room(evt.room_id)
|
||||||
|
await self._update_remote_profile()
|
||||||
|
else:
|
||||||
|
self.log.debug('Inserting invite into database for later handling')
|
||||||
|
DBInvite(
|
||||||
|
client=self.id,
|
||||||
|
room=evt.room_id,
|
||||||
|
date=datetime.fromtimestamp(evt.timestamp//1000),
|
||||||
|
inviter=evt.sender
|
||||||
|
).upsert()
|
||||||
|
|
||||||
async def update_started(self, started: bool) -> None:
|
async def update_started(self, started: bool) -> None:
|
||||||
if started is None or started == self.started:
|
if started is None or started == self.started:
|
||||||
|
@ -307,6 +329,26 @@ class Client:
|
||||||
else:
|
else:
|
||||||
await self._update_remote_profile()
|
await self._update_remote_profile()
|
||||||
|
|
||||||
|
async def join_room(self, room: RoomID) -> None:
|
||||||
|
if room is None:
|
||||||
|
return
|
||||||
|
await self.client.join_room(room)
|
||||||
|
DBInvite(client=self.id, room=room).delete()
|
||||||
|
await self._update_remote_profile()
|
||||||
|
|
||||||
|
async def leave_room(self, room: RoomID) -> None:
|
||||||
|
if room is None:
|
||||||
|
return
|
||||||
|
await self.client.leave_room(room)
|
||||||
|
DBInvite(client=self.id, room=room).delete()
|
||||||
|
await self._update_remote_profile()
|
||||||
|
|
||||||
|
async def ignore_invite(self, room: RoomID) -> None:
|
||||||
|
if room is None:
|
||||||
|
return
|
||||||
|
DBInvite(client=self.id, room=room).delete()
|
||||||
|
await self._update_remote_profile()
|
||||||
|
|
||||||
async def update_access_details(self, access_token: Optional[str], homeserver: Optional[str],
|
async def update_access_details(self, access_token: Optional[str], homeserver: Optional[str],
|
||||||
device_id: Optional[str] = None) -> None:
|
device_id: Optional[str] = None) -> None:
|
||||||
if not access_token and not homeserver:
|
if not access_token and not homeserver:
|
||||||
|
@ -354,6 +396,7 @@ class Client:
|
||||||
async def _update_remote_profile(self) -> None:
|
async def _update_remote_profile(self) -> None:
|
||||||
profile = await self.client.get_profile(self.id)
|
profile = await self.client.get_profile(self.id)
|
||||||
self.remote_displayname, self.remote_avatar_url = profile.displayname, profile.avatar_url
|
self.remote_displayname, self.remote_avatar_url = profile.displayname, profile.avatar_url
|
||||||
|
self.remote_rooms = await self.client.get_joined_rooms()
|
||||||
|
|
||||||
# region Properties
|
# region Properties
|
||||||
|
|
||||||
|
@ -412,12 +455,12 @@ class Client:
|
||||||
def autojoin(self, value: bool) -> None:
|
def autojoin(self, value: bool) -> None:
|
||||||
if value == self.db_instance.autojoin:
|
if value == self.db_instance.autojoin:
|
||||||
return
|
return
|
||||||
if value:
|
|
||||||
self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
|
||||||
else:
|
|
||||||
self.client.remove_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
|
||||||
self.db_instance.autojoin = value
|
self.db_instance.autojoin = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def invites(self) -> List[DBInvite]:
|
||||||
|
return DBInvite.get(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def online(self) -> bool:
|
def online(self) -> bool:
|
||||||
return self.db_instance.online
|
return self.db_instance.online
|
||||||
|
|
38
maubot/db.py
38
maubot/db.py
|
@ -16,12 +16,13 @@
|
||||||
from typing import Iterable, Optional
|
from typing import Iterable, Optional
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Column, String, Boolean, ForeignKey, Text
|
from sqlalchemy import Column, String, Boolean, ForeignKey, Text, DateTime
|
||||||
from sqlalchemy.engine.base import Engine
|
from sqlalchemy.engine.base import Engine
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
|
|
||||||
from mautrix.types import UserID, FilterID, DeviceID, SyncToken, ContentURI
|
from mautrix.types import UserID, RoomID, FilterID, DeviceID, SyncToken, ContentURI
|
||||||
from mautrix.util.db import Base
|
from mautrix.util.db import Base
|
||||||
from mautrix.client.state_store.sqlalchemy import RoomState, UserProfile
|
from mautrix.client.state_store.sqlalchemy import RoomState, UserProfile
|
||||||
|
|
||||||
|
@ -76,11 +77,42 @@ class DBClient(Base):
|
||||||
return cls._select_one_or_none(cls.c.id == id)
|
return cls._select_one_or_none(cls.c.id == id)
|
||||||
|
|
||||||
|
|
||||||
|
class DBInvite(Base):
|
||||||
|
__tablename__ = "invite"
|
||||||
|
|
||||||
|
client: UserID = Column(String(255), ForeignKey("client.id", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||||
|
room: RoomID = Column(String(255), nullable=False, primary_key=True)
|
||||||
|
date: datetime = Column(DateTime(), nullable=False)
|
||||||
|
inviter: UserID = Column(String(255), nullable=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> Iterable['DBInvite']:
|
||||||
|
return cls._select_all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, client: UserID, room: RoomID) -> Optional['DBInvite']:
|
||||||
|
return cls._select_one_or_none(cls.c.client == client, cls.c.room == room)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, client: UserID) -> Iterable['DBInvite']:
|
||||||
|
return cls._select_all(cls.c.client == client)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_tombstone(cls, client: UserID, old: RoomID, new: RoomID) -> Optional['DBInvite']:
|
||||||
|
invite = cls.get(client=client, room=old)
|
||||||
|
if not invite:
|
||||||
|
return None
|
||||||
|
invite.delete()
|
||||||
|
invite.room = new
|
||||||
|
invite.insert()
|
||||||
|
return invite
|
||||||
|
|
||||||
|
|
||||||
def init(config: Config) -> Engine:
|
def init(config: Config) -> Engine:
|
||||||
db = sql.create_engine(config["database"])
|
db = sql.create_engine(config["database"])
|
||||||
Base.metadata.bind = db
|
Base.metadata.bind = db
|
||||||
|
|
||||||
for table in (DBPlugin, DBClient, RoomState, UserProfile):
|
for table in (DBPlugin, DBClient, RoomState, UserProfile, DBInvite):
|
||||||
table.bind(db)
|
table.bind(db)
|
||||||
|
|
||||||
if not db.has_table("alembic_version"):
|
if not db.has_table("alembic_version"):
|
||||||
|
|
|
@ -89,11 +89,7 @@ async def _update_client(client: Client, data: dict, is_login: bool = False) ->
|
||||||
except MatrixConnectionError:
|
except MatrixConnectionError:
|
||||||
return resp.bad_client_connection_details
|
return resp.bad_client_connection_details
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
str_err = str(e)
|
return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):])
|
||||||
if str_err.startswith("MXID mismatch"):
|
|
||||||
return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):])
|
|
||||||
elif str_err.startswith("Device ID mismatch"):
|
|
||||||
return resp.device_id_mismatch(str(e)[len("Device ID mismatch: "):])
|
|
||||||
with client.db_instance.edit_mode():
|
with client.db_instance.edit_mode():
|
||||||
await client.update_avatar_url(data.get("avatar_url", None))
|
await client.update_avatar_url(data.get("avatar_url", None))
|
||||||
await client.update_displayname(data.get("displayname", None))
|
await client.update_displayname(data.get("displayname", None))
|
||||||
|
@ -155,3 +151,30 @@ async def clear_client_cache(request: web.Request) -> web.Response:
|
||||||
return resp.client_not_found
|
return resp.client_not_found
|
||||||
client.clear_cache()
|
client.clear_cache()
|
||||||
return resp.ok
|
return resp.ok
|
||||||
|
|
||||||
|
|
||||||
|
@routes.post("/client/{id}/room/{room}/join")
|
||||||
|
async def join_room(request: web.Request) -> web.Response:
|
||||||
|
user_id = request.match_info.get("id", None)
|
||||||
|
room_id = request.match_info.get("room", None)
|
||||||
|
client = Client.get(user_id, None)
|
||||||
|
await client.join_room(room_id)
|
||||||
|
return resp.updated(client.to_dict(), is_login=False)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.post("/client/{id}/room/{room}/leave")
|
||||||
|
async def leave_room(request: web.Request) -> web.Response:
|
||||||
|
user_id = request.match_info.get("id", None)
|
||||||
|
room_id = request.match_info.get("room", None)
|
||||||
|
client = Client.get(user_id, None)
|
||||||
|
await client.leave_room(room_id)
|
||||||
|
return resp.updated(client.to_dict(), is_login=False)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.post("/client/{id}/room/{room}/ignore")
|
||||||
|
async def leave_room(request: web.Request) -> web.Response:
|
||||||
|
user_id = request.match_info.get("id", None)
|
||||||
|
room_id = request.match_info.get("room", None)
|
||||||
|
client = Client.get(user_id, None)
|
||||||
|
await client.ignore_invite(room_id)
|
||||||
|
return resp.updated(client.to_dict(), is_login=False)
|
||||||
|
|
|
@ -425,6 +425,93 @@ paths:
|
||||||
$ref: '#/components/responses/Unauthorized'
|
$ref: '#/components/responses/Unauthorized'
|
||||||
404:
|
404:
|
||||||
$ref: '#/components/responses/ClientNotFound'
|
$ref: '#/components/responses/ClientNotFound'
|
||||||
|
'/client/{id}/room/{room}/join':
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The Matrix user ID of the client to join a room with
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: room
|
||||||
|
in: path
|
||||||
|
description: The Matrix room to join
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
post:
|
||||||
|
operationId: join_room
|
||||||
|
summary: Join a Matrix room or accept an invite
|
||||||
|
tags: [Clients]
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Joined the room
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/MatrixClient'
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/ClientNotFound'
|
||||||
|
'/client/{id}/room/{room}/leave':
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The Matrix user ID of the client to leave a room with
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The Matrix room to leave or reject
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
post:
|
||||||
|
operationId: leave_room
|
||||||
|
summary: Leave a Matrix room or reject an invite
|
||||||
|
tags: [Clients]
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Left the room
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/MatrixClient'
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/ClientNotFound'
|
||||||
|
'/client/{id}/room/{room}/ignore':
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The Matrix user ID of the client to remove the invite from
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The Matrix room whose invite to remove
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
post:
|
||||||
|
operationId: ignore_invite
|
||||||
|
summary: Ignore a room invitation (do not answer it, but remove from the database)
|
||||||
|
tags: [Clients]
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Removed the invitation.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/MatrixClient'
|
||||||
|
401:
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
404:
|
||||||
|
$ref: '#/components/responses/ClientNotFound'
|
||||||
/client/auth/servers:
|
/client/auth/servers:
|
||||||
get:
|
get:
|
||||||
operationId: get_client_auth_servers
|
operationId: get_client_auth_servers
|
||||||
|
@ -710,6 +797,36 @@ components:
|
||||||
type: string
|
type: string
|
||||||
example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU'
|
example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU'
|
||||||
description: The content URI of the avatar for this client.
|
description: The content URI of the avatar for this client.
|
||||||
|
rooms:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
description: List of room IDs of currently joined room.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
example: '!foobar:example.org'
|
||||||
|
invites:
|
||||||
|
type: array
|
||||||
|
readOnly: true
|
||||||
|
description: List of pending invites.
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
client:
|
||||||
|
type: string
|
||||||
|
example: '@foobar:example.org'
|
||||||
|
description: The client that was invited to a room.
|
||||||
|
room:
|
||||||
|
type: string
|
||||||
|
example: '!foobar:example.org'
|
||||||
|
description: The room the client was invited to.
|
||||||
|
date:
|
||||||
|
type: integer
|
||||||
|
example: 1642546523
|
||||||
|
description: UNIX timestamp when the invite was created.
|
||||||
|
inviter:
|
||||||
|
type: string
|
||||||
|
example: '@bar:example.org'
|
||||||
|
description: The user who invited this client to the room.
|
||||||
instances:
|
instances:
|
||||||
type: array
|
type: array
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
|
|
@ -233,6 +233,30 @@ export async function clearClientCache(id) {
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function joinRoom(id, room) {
|
||||||
|
const resp = await fetch(`${BASE_PATH}/client/${id}/room/${room}/join`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
return await resp.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function leaveRoom(id, room) {
|
||||||
|
const resp = await fetch(`${BASE_PATH}/client/${id}/room/${room}/leave`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
return await resp.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ignoreInvite(id, room) {
|
||||||
|
const resp = await fetch(`${BASE_PATH}/client/${id}/room/${room}/ignore`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
return await resp.json()
|
||||||
|
}
|
||||||
|
|
||||||
export const getClientAuthServers = () => defaultGet("/client/auth/servers")
|
export const getClientAuthServers = () => defaultGet("/client/auth/servers")
|
||||||
|
|
||||||
export async function doClientAuth(server, type, username, password) {
|
export async function doClientAuth(server, type, username, password) {
|
||||||
|
@ -253,5 +277,5 @@ export default {
|
||||||
getInstanceDatabase, queryInstanceDatabase,
|
getInstanceDatabase, queryInstanceDatabase,
|
||||||
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
||||||
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, clearClientCache,
|
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, clearClientCache,
|
||||||
getClientAuthServers, doClientAuth,
|
joinRoom, leaveRoom, ignoreInvite, getClientAuthServers, doClientAuth,
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ class Client extends BaseMainView {
|
||||||
|
|
||||||
get entryKeys() {
|
get entryKeys() {
|
||||||
return ["id", "displayname", "homeserver", "avatar_url", "access_token", "device_id",
|
return ["id", "displayname", "homeserver", "avatar_url", "access_token", "device_id",
|
||||||
"sync", "autojoin", "online", "enabled", "started"]
|
"sync", "autojoin", "online", "enabled", "started", "rooms", "invites"]
|
||||||
}
|
}
|
||||||
|
|
||||||
get initialState() {
|
get initialState() {
|
||||||
|
@ -81,6 +81,11 @@ class Client extends BaseMainView {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
online: true,
|
online: true,
|
||||||
started: false,
|
started: false,
|
||||||
|
rooms: [],
|
||||||
|
invites: [],
|
||||||
|
joiningRooms: [],
|
||||||
|
ignoringRooms: [],
|
||||||
|
leavingRooms: [],
|
||||||
|
|
||||||
instances: [],
|
instances: [],
|
||||||
|
|
||||||
|
@ -102,6 +107,9 @@ class Client extends BaseMainView {
|
||||||
delete client.clearingCache
|
delete client.clearingCache
|
||||||
delete client.error
|
delete client.error
|
||||||
delete client.instances
|
delete client.instances
|
||||||
|
delete client.joiningRooms
|
||||||
|
delete client.ignoringRooms
|
||||||
|
delete client.leavingRooms
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +196,75 @@ class Client extends BaseMainView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joinRoom = async room => {
|
||||||
|
if (this.state.joiningRooms.includes(room)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
joiningRooms: this.state.joiningRooms.concat([room]),
|
||||||
|
})
|
||||||
|
const resp = await api.joinRoom(this.state.id, room)
|
||||||
|
if (resp.id) {
|
||||||
|
this.setState({
|
||||||
|
joiningRooms: this.state.joiningRooms.filter(jr => jr !== room),
|
||||||
|
invites: resp.invites,
|
||||||
|
rooms: resp.rooms,
|
||||||
|
error: "",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
joiningRooms: this.state.joiningRooms.filter(jr => jr !== room),
|
||||||
|
error: resp.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom = async room => {
|
||||||
|
if (this.state.leavingRooms.includes(room)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
leavingRooms: this.state.leavingRooms.concat([room]),
|
||||||
|
})
|
||||||
|
const resp = await api.leaveRoom(this.state.id, room)
|
||||||
|
if (resp.id) {
|
||||||
|
this.setState({
|
||||||
|
leavingRooms: this.state.leavingRooms.filter(jr => jr !== room),
|
||||||
|
invites: resp.invites,
|
||||||
|
rooms: resp.rooms,
|
||||||
|
error: "",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
leavingRooms: this.state.leavingRooms.filter(jr => jr !== room),
|
||||||
|
error: resp.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreInvite = async room => {
|
||||||
|
if (this.state.ignoringRooms.includes(room)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
ignoringRooms: this.state.ignoringRooms.concat([room]),
|
||||||
|
})
|
||||||
|
const resp = await api.ignoreInvite(this.state.id, room)
|
||||||
|
if (resp.id) {
|
||||||
|
this.setState({
|
||||||
|
ignoringRooms: this.state.ignoringRooms.filter(jr => jr !== room),
|
||||||
|
invites: resp.invites,
|
||||||
|
rooms: resp.rooms,
|
||||||
|
error: "",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
ignoringRooms: this.state.ignoringRooms.filter(jr => jr !== room),
|
||||||
|
error: resp.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get loading() {
|
get loading() {
|
||||||
return this.state.saving || this.state.startingOrStopping
|
return this.state.saving || this.state.startingOrStopping
|
||||||
|| this.clearingCache || this.state.deleting
|
|| this.clearingCache || this.state.deleting
|
||||||
|
@ -325,6 +402,54 @@ class Client extends BaseMainView {
|
||||||
<div className="error">{this.state.error}</div>
|
<div className="error">{this.state.error}</div>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
renderRoomsAndInvites = () => <>
|
||||||
|
<div className="rooms">
|
||||||
|
<h3>Joined Rooms</h3>
|
||||||
|
<table class="rooms-list">
|
||||||
|
{this.state.rooms.map((room, i) =>
|
||||||
|
<tr>
|
||||||
|
<td>{room}</td>
|
||||||
|
<td class="right">
|
||||||
|
<button class="button error-color" onClick={ev => this.leaveRoom(room)}
|
||||||
|
disabled={this.state.leavingRooms.includes(room)}
|
||||||
|
title={"Leave this room"}>
|
||||||
|
{this.state.leavingRooms.includes(room) ? <Spinner/> : "✗"}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
{this.state.rooms.length === 0 ? <span class="no-rooms">Not in any rooms.</span> : ""}
|
||||||
|
<h3>Pending Invites</h3>
|
||||||
|
<table class="rooms-list">
|
||||||
|
{this.state.invites.map((invite, i) =>
|
||||||
|
<tr>
|
||||||
|
<td>{invite.room}</td>
|
||||||
|
<td class="right">
|
||||||
|
<button class="button main-color" title={"Accept this invite"}
|
||||||
|
onClick={ev => this.joinRoom(invite.room)}
|
||||||
|
disabled={this.state.joiningRooms.includes(invite.room)}>
|
||||||
|
{this.state.joiningRooms.includes(invite.room) ? <Spinner/> : "✓"}
|
||||||
|
</button>
|
||||||
|
<button class="button warning-color" title={"Ignore this invite"}
|
||||||
|
onClick={ev => this.ignoreInvite(invite.room)}
|
||||||
|
disabled={this.state.ignoringRooms.includes(invite.room)}>
|
||||||
|
{this.state.ignoringRooms.includes(invite.room) ? <Spinner/> : "🗑"}
|
||||||
|
</button>
|
||||||
|
<button class="button error-color" title={"Reject this invite"}
|
||||||
|
onClick={ev => this.leaveRoom(invite.room)}
|
||||||
|
disabled={this.state.leavingRooms.includes(invite.room)}>
|
||||||
|
{this.state.leavingRooms.includes(invite.room) ? <Spinner/> : "✗"}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
)}
|
||||||
|
</table>
|
||||||
|
{this.state.invites.length === 0 ?
|
||||||
|
<span class="no-rooms">No pending invites.</span> : ""}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <>
|
return <>
|
||||||
<div className="client">
|
<div className="client">
|
||||||
|
@ -333,6 +458,7 @@ class Client extends BaseMainView {
|
||||||
{this.renderPreferences()}
|
{this.renderPreferences()}
|
||||||
{this.renderPrefButtons()}
|
{this.renderPrefButtons()}
|
||||||
{this.renderInstances()}
|
{this.renderInstances()}
|
||||||
|
{this.renderRoomsAndInvites()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -50,12 +50,38 @@
|
||||||
background-color: $background-dark !important
|
background-color: $background-dark !important
|
||||||
color: $text-color
|
color: $text-color
|
||||||
|
|
||||||
|
=error-color-button()
|
||||||
|
background-color: $error-light
|
||||||
|
color: $inverted-text-color
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background-color: $error
|
||||||
|
|
||||||
|
&:disabled.disabled-bg
|
||||||
|
background-color: $background-dark !important
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
|
=warning-color-button()
|
||||||
|
background-color: $warning
|
||||||
|
color: $inverted-text-color
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background-color: $warning-dark
|
||||||
|
|
||||||
|
&:disabled.disabled-bg
|
||||||
|
background-color: $background-dark !important
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
.button
|
.button
|
||||||
+button
|
+button
|
||||||
|
|
||||||
&.main-color
|
&.main-color
|
||||||
+main-color-button
|
+main-color-button
|
||||||
|
|
||||||
|
&.error-color
|
||||||
|
+error-color-button
|
||||||
|
|
||||||
|
&.warning-color
|
||||||
|
+warning-color-button
|
||||||
|
|
||||||
=button-group()
|
=button-group()
|
||||||
width: 100%
|
width: 100%
|
||||||
display: flex
|
display: flex
|
||||||
|
|
|
@ -24,6 +24,7 @@ $error: #B71C1C
|
||||||
$error-dark: #7F0000
|
$error-dark: #7F0000
|
||||||
$error-light: #F05545
|
$error-light: #F05545
|
||||||
$warning: orange
|
$warning: orange
|
||||||
|
$warning-dark: #bf7b00
|
||||||
|
|
||||||
$border-color: #DDD
|
$border-color: #DDD
|
||||||
$text-color: #212121
|
$text-color: #212121
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
@import pages/mixins/upload-container
|
@import pages/mixins/upload-container
|
||||||
@import pages/mixins/instancelist
|
@import pages/mixins/instancelist
|
||||||
|
@import pages/mixins/roomlist
|
||||||
|
|
||||||
@import pages/login
|
@import pages/login
|
||||||
@import pages/dashboard
|
@import pages/dashboard
|
||||||
|
|
|
@ -36,6 +36,9 @@
|
||||||
> div.instances
|
> div.instances
|
||||||
+instancelist
|
+instancelist
|
||||||
|
|
||||||
|
> div.rooms
|
||||||
|
+roomlist
|
||||||
|
|
||||||
input.fingerprint
|
input.fingerprint
|
||||||
font-family: "Fira Code", monospace
|
font-family: "Fira Code", monospace
|
||||||
font-size: 0.8em
|
font-size: 0.8em
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
=roomlist()
|
||||||
|
margin: 1rem 0
|
||||||
|
|
||||||
|
> h3
|
||||||
|
margin: .5rem
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
> span.no-rooms
|
||||||
|
margin: .5rem
|
||||||
|
font-size: .875rem
|
||||||
|
font-weight: lighter
|
||||||
|
|
||||||
|
> table.rooms-list
|
||||||
|
width: 100%
|
||||||
|
margin: .5rem
|
||||||
|
border-collapse: collapse
|
||||||
|
text-align: left
|
||||||
|
|
||||||
|
td.right
|
||||||
|
display: flex
|
||||||
|
justify-content: flex-end
|
||||||
|
align-content: flex-end
|
||||||
|
flex-direction: row
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
.button
|
||||||
|
margin: .1rem
|
||||||
|
height: 31px
|
||||||
|
width: 48px
|
||||||
|
|
||||||
|
> .spinner
|
||||||
|
+thick-spinner
|
||||||
|
+white-spinner
|
||||||
|
width: 16px
|
||||||
|
|
||||||
|
> tr
|
||||||
|
border-top: 1px solid $border-color
|
Loading…
Reference in a new issue