Merge branch 'master' into human-id-rules
commit
0131543f00
@ -1,303 +0,0 @@
|
|||||||
#1: Lightweight IM client (no perm storage)
|
|
||||||
#2: Mobile IM client (perm storage)
|
|
||||||
#3: MIDI client
|
|
||||||
#4: Animatrix client
|
|
||||||
#5: Unity object trees
|
|
||||||
#6: Forum
|
|
||||||
#7: Social Network ("Walls", PMs, groups)
|
|
||||||
#8: Minecraft-clone
|
|
||||||
#9: Bug Tracking Software
|
|
||||||
#10: Global 'Like' widget, which links through to a room.
|
|
||||||
|
|
||||||
================
|
|
||||||
|
|
||||||
#1: Lightweight IM client (no perm storage)
|
|
||||||
-------------------------------------------
|
|
||||||
Description:
|
|
||||||
An IM client (think web client) with no way of persisting data beyond
|
|
||||||
a session (the instance a person is using the app).
|
|
||||||
Features:
|
|
||||||
Recent activity, Room screen (member list, etc), User page, just like
|
|
||||||
the web client.
|
|
||||||
Actions:
|
|
||||||
- Send a one-to-one message to someone.
|
|
||||||
- Accept an invite.
|
|
||||||
- Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
|
||||||
- Populate scrollback if click on room
|
|
||||||
- Populate member list if click on room + get presence updates for them
|
|
||||||
- Populate room name / topic if click on room
|
|
||||||
- Create an empty room.
|
|
||||||
- Join a room from an alias.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Send a one-to-one message to someone.
|
|
||||||
How:
|
|
||||||
Enter their username and hit Message. Taken to room page with invited user.
|
|
||||||
History displays that I've invited someone / joined the room. Enter a message
|
|
||||||
and hit send. Message appears in window.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
-- @user:domain -->
|
|
||||||
<--- room ID, ACK--
|
|
||||||
<-historical msgs--
|
|
||||||
-- msg,room ID --->
|
|
||||||
<--- ACK ----------
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Accept an invite.
|
|
||||||
How:
|
|
||||||
Get list of invites. Click one of them to 'accept' it. May or may not want
|
|
||||||
room content.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- req invites ->
|
|
||||||
<--- [inv,inv] ----
|
|
||||||
---- accept inv -->
|
|
||||||
<--- ACK ----------
|
|
||||||
<--- room content-- (optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
|
||||||
How:
|
|
||||||
Request joined rooms with latest message and room name. Request rooms invited to. Possibly extra info like # joined members.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- req sync ---->
|
|
||||||
<---joined rooms--- {msg,name,alias,#members?}
|
|
||||||
<---invited rooms-- {name,alias}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate scrollback if click on room.
|
|
||||||
How:
|
|
||||||
Request scrollback for room.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- room id ----->
|
|
||||||
<--- scrollback ---
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate member list if click on room + get presence updates for them.
|
|
||||||
How:
|
|
||||||
Click on room. Member list with names/presence/pics appears. May not want
|
|
||||||
pic.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- req mem list ->
|
|
||||||
<--- members ------- {name,pic,presence}
|
|
||||||
- monitor presence->
|
|
||||||
...
|
|
||||||
<- presence change--
|
|
||||||
<- presence change--
|
|
||||||
...
|
|
||||||
-- stop presence --->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate room name / topic if click on room.
|
|
||||||
How:
|
|
||||||
Click on room. Room name and topic with aliases appears. May not want topic
|
|
||||||
(eg screen size).
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- req room info->
|
|
||||||
<--- room info ----- {name,topic,aliases}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Create an empty room.
|
|
||||||
How:
|
|
||||||
Type in room config (desired name, public/private, etc). Hit Create. Room is
|
|
||||||
created. Possibly get room info.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- mkroom{config}->
|
|
||||||
<--ACK{room_id}------
|
|
||||||
<-- room info ------- (optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Join a room from an alias.
|
|
||||||
How:
|
|
||||||
Type in alias. Hit Join. Room is joined. Possibly get room info.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
-- join{alias} ----->
|
|
||||||
<--ACK{room_id}------
|
|
||||||
<--room info--------- (optional)
|
|
||||||
|
|
||||||
|
|
||||||
===========================
|
|
||||||
|
|
||||||
#2: Mobile IM client (perm storage)
|
|
||||||
-----------------------------------
|
|
||||||
Description:
|
|
||||||
An IM client (think android/ios) which persists data on a database.
|
|
||||||
Features:
|
|
||||||
Recent activity, Room screen (member list, etc), User page, just like
|
|
||||||
the web client.
|
|
||||||
Actions:
|
|
||||||
- Send a one-to-one message to someone.
|
|
||||||
- Accept a stored invite.
|
|
||||||
- Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
|
||||||
- Populate scrollback if click on room
|
|
||||||
- Populate member list if click on room + get presence updates for them
|
|
||||||
- Populate room name / topic if click on room
|
|
||||||
- Create an empty room.
|
|
||||||
- Join a room from an alias.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Send a one-to-one message to someone (single room).
|
|
||||||
How:
|
|
||||||
Enter their username and hit Message. Taken to room page with invited user if no room exists,
|
|
||||||
else takes to existing room. History displays that I've invited someone or scrollback. Enter
|
|
||||||
a message and hit send. Message appears in window.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
-- @user:domain -->
|
|
||||||
<--- room ID, ACK--
|
|
||||||
<-historical msgs-- (optional; not if existing room)
|
|
||||||
-- msg,room ID --->
|
|
||||||
<--- ACK ----------
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Accept a stored invite.
|
|
||||||
How:
|
|
||||||
Send invite to server. Get room content (or NO-OP if already joined).
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- accept inv -->
|
|
||||||
<--- ACK ----------
|
|
||||||
<--- room content-- (optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate recent activity (all rooms joined with latest message + room names/aliases, rooms invited to + room names/aliases)
|
|
||||||
incrementally.
|
|
||||||
How:
|
|
||||||
Request recent activity diff. Get updated msg/name/#members for changed values only.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
- req sync{token}->
|
|
||||||
<---diff{rooms}---- {msg,name,alias,#members?}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate scrollback if click on room.
|
|
||||||
How:
|
|
||||||
Request scrollback for room. Either a diff or a page of scrollback
|
|
||||||
depending on cached data.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
-room id{latest event}-> {max msgs}
|
|
||||||
<--- scrollback -------- {fresh/incremental flag}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate member list if click on room + get presence updates for them.
|
|
||||||
How:
|
|
||||||
Click on room. Member list with names/presence/pics appears. May not want
|
|
||||||
pic.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- req mem list ->
|
|
||||||
<--- members ------- {name,pic,presence}
|
|
||||||
- monitor presence->
|
|
||||||
...
|
|
||||||
<- presence change--
|
|
||||||
<- presence change--
|
|
||||||
...
|
|
||||||
-- stop presence --->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Populate room name / topic if click on room.
|
|
||||||
How:
|
|
||||||
Click on room. Room name and topic with aliases appears. May not want topic
|
|
||||||
(eg screen size). Display cached info until updated.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- req room info->
|
|
||||||
<--- room info ----- {name,topic,aliases}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Create an empty room.
|
|
||||||
How:
|
|
||||||
Type in room config (desired name, public/private, etc). Hit Create. Room is
|
|
||||||
created. Possibly get room info.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
---- mkroom{config}->
|
|
||||||
<--ACK{room_id}------
|
|
||||||
<-- room info ------- (optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action:
|
|
||||||
Join a room from an alias.
|
|
||||||
How:
|
|
||||||
Type in alias. Hit Join. Room is joined. Possibly get room info.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Client Server
|
|
||||||
-- join{alias} ----->
|
|
||||||
<--ACK{room_id}------
|
|
||||||
<--room info--------- (optional)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
|||||||
|
Data flows for use cases
|
||||||
|
========================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
<- Data from server to client
|
||||||
|
-> Data from client to server
|
||||||
|
|
||||||
|
Instant Messaging
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Without storage
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Home screen
|
||||||
|
Data required on load:
|
||||||
|
<- For each room the user is joined: Name, topic, # members, last message, room ID, aliases
|
||||||
|
Data required when new message arrives for a room:
|
||||||
|
<- Room ID, message content, sender (user ID, display name, avatar url)
|
||||||
|
Data required when someone invites you to a room:
|
||||||
|
<- Room ID, sender (user ID, display name, avatar url), Room Name, Room Topic
|
||||||
|
Data required when you leave a room on another device:
|
||||||
|
<- Room ID
|
||||||
|
Data required when you join a room on another device:
|
||||||
|
<- Name, topic, # members, last message, room ID, aliases
|
||||||
|
Data required when your profile info changes on another device:
|
||||||
|
<- new profile info e.g. avatar, display name, etc.
|
||||||
|
|
||||||
|
Creating a room
|
||||||
|
-> Invitee list of user IDs, public/private, name of room, alias of room, topic of room
|
||||||
|
<- Room ID
|
||||||
|
|
||||||
|
Joining a room (and dumped into chat screen on success)
|
||||||
|
-> Room ID / Room alias
|
||||||
|
<- Room ID, Room aliases (plural), Name, topic, member list (f.e. member: user ID,
|
||||||
|
avatar, presence, display name, power level, whether they are typing), enough
|
||||||
|
messages to fill screen (and whether there are more)
|
||||||
|
|
||||||
|
Chat Screen
|
||||||
|
Data required when member name changes:
|
||||||
|
<- new name, room ID, user ID, when in the context of the room did this occur
|
||||||
|
Data required when the room name changes:
|
||||||
|
<- new name, room ID, old room name?
|
||||||
|
Invite a user:
|
||||||
|
-> user ID, room ID
|
||||||
|
<- display name / avatar of user invited (if known)
|
||||||
|
Kick a user:
|
||||||
|
-> user ID, room ID
|
||||||
|
<- what message it came after
|
||||||
|
Leave a room:
|
||||||
|
-> room ID
|
||||||
|
<- what message it came after
|
||||||
|
|
||||||
|
Send a message
|
||||||
|
-> Message content, room ID, message sequencing (eg sending my 1st, 2nd, 3rd msg)
|
||||||
|
<- actual content sent (if server mods it), what message it comes after (to correctly
|
||||||
|
display the local echo)
|
||||||
|
|
||||||
|
Place a call (receive a call is just reverse)
|
||||||
|
<- turn servers
|
||||||
|
-> SDP offer
|
||||||
|
-> Ice candidates (1 by 1; trickling)
|
||||||
|
<- SDP answer
|
||||||
|
<- Ice candidates
|
||||||
|
|
||||||
|
Scrolling back (infinite scrolling)
|
||||||
|
-> Identifier for the earliest message, # requested messages
|
||||||
|
<- requested messages (f.e change in display name, what the old name was), whether
|
||||||
|
there are more.
|
||||||
|
|
||||||
|
|
||||||
|
With storage
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
::
|
||||||
|
|
||||||
|
Home Screen
|
||||||
|
On Load
|
||||||
|
-> Identifier which tells the server the client's current state (which rooms it is aware
|
||||||
|
of, which messages it has, what display names for users, etc..)
|
||||||
|
<- A delta from the client's current state to the current state on the server (e.g. the
|
||||||
|
new rooms, the *latest* message if different, the changed display names, the new
|
||||||
|
invites, etc). f.e Room: Whether the cache of the room that you have has been replaced
|
||||||
|
with this new state.
|
||||||
|
|
||||||
|
Pre-load optimisation (not essential for this screen)
|
||||||
|
-> Number of desired messages f.e room to cache
|
||||||
|
<- f.e Room: the delta OR the entire state
|
||||||
|
|
||||||
|
|
||||||
|
Bug Tracking
|
||||||
|
------------
|
||||||
|
::
|
||||||
|
|
||||||
|
Landing Page
|
||||||
|
On Load
|
||||||
|
<- Issues assigned to me, Issues I'm watching, Recent activity on other issues includes
|
||||||
|
comments, list of projects
|
||||||
|
|
||||||
|
Search for an issue (assume text)
|
||||||
|
-> Search string
|
||||||
|
<- List of paginated issues
|
||||||
|
Request page 2:
|
||||||
|
-> Page number requested
|
||||||
|
<- Page of paginated issues
|
||||||
|
|
||||||
|
Issue Page
|
||||||
|
On Load
|
||||||
|
-> Issue ID and Project ID (equiv to Room)
|
||||||
|
<- Issue contents e.g. priority, resolution state, etc. All comments e.g. user ID,
|
||||||
|
comment text, timestamp. Entire issue history e.g. changes in priority
|
||||||
|
|
||||||
|
Post a comment
|
||||||
|
-> Issue ID, comment content, Project ID (equiv to Room)
|
||||||
|
<- actual content sent (if modded), what comment it comes after
|
||||||
|
|
||||||
|
Set issue priority
|
||||||
|
-> Issue ID, Project ID, desired priority
|
||||||
|
<- What action in the history it came after
|
||||||
|
|
||||||
|
Someone else sets issue priority
|
||||||
|
<- Issue ID, Project ID, new priority, where in the history
|
||||||
|
|
||||||
|
|
||||||
|
Mapping model use cases to matrix models (Room, Message, etc)
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
To think about:
|
||||||
|
- Do we want to support the idea of forking off new rooms from existing ones? This
|
||||||
|
and forums could benefit from it.
|
||||||
|
|
||||||
|
Bug tracking UI
|
||||||
|
---------------
|
||||||
|
::
|
||||||
|
|
||||||
|
Projects => Rooms
|
||||||
|
Issues => Message Events
|
||||||
|
Comments => Message Events (relates_to key)
|
||||||
|
|
||||||
|
Projects:
|
||||||
|
- Unlikely that there will be 100,000s of issues, so having to pull in all the issues for a project is okay.
|
||||||
|
- Permissions are usually per project and this Just Works.
|
||||||
|
- New issues come in automatically and Just Work.
|
||||||
|
- Can have read-only members
|
||||||
|
|
||||||
|
Issues:
|
||||||
|
- Don't really want 1 Room per Issue, else you can have thousands of Rooms PER PROJECT, hence choice for
|
||||||
|
Issues as Messages. Don't need to join a room for each issue.
|
||||||
|
- Idea of issue owner is clear (sender of the message)
|
||||||
|
- Updating issues requires an additional event similar to comments (with ``relates_to``)? Could possibly
|
||||||
|
be state events? Don't really want all the history if say the priority was changed 1000 times, just want
|
||||||
|
the current state of the key.
|
||||||
|
|
||||||
|
Comments:
|
||||||
|
- Additional event with ``relates_to`` key.
|
||||||
|
|
||||||
|
|
||||||
|
Forum
|
||||||
|
-----
|
||||||
|
::
|
||||||
|
|
||||||
|
Forum => Room (with pointers to Board Rooms)
|
||||||
|
Boards => Room (with pointers to Thread Rooms)
|
||||||
|
Threads => Room
|
||||||
|
Messages => Message Events
|
||||||
|
|
||||||
|
Forum:
|
||||||
|
- Contains 10s of Boards.
|
||||||
|
- Contains special Message Events which point to different rooms f.e Board.
|
||||||
|
|
||||||
|
Boards:
|
||||||
|
- Contains 100s of Threads.
|
||||||
|
- Contains special Message Events which point to different rooms f.e. Thread.
|
||||||
|
|
||||||
|
Threads:
|
||||||
|
- Contains 100s of Messages.
|
||||||
|
|
||||||
|
Can't do this nicely with the current Federation API because you have loads of
|
||||||
|
Rooms and what does posting a message look like? Creating a thread is done by..?
|
||||||
|
The user who is posting cannot create the thread because otherwise they would be
|
||||||
|
the room creator and have ultimate privileges. So it has to be created by a bot
|
||||||
|
of some kind which ties into auth (Application services?). To follow a board,
|
||||||
|
you need a bot to join the Board Room and then watch it for changes...
|
||||||
|
|
||||||
|
Fundamental problem with forums is that there is only 1 PDU graph per room and
|
||||||
|
you either have to pull in lots of graphs separately or one graph and filter it
|
||||||
|
separately to get to the desired sub set of data. You have to subscribe into a
|
||||||
|
lot of graphs if you subscribe to a board... If you have the entire board...
|
||||||
|
good luck scrollbacking a particular thread.
|
||||||
|
|
||||||
|
|
||||||
|
Google+ Community
|
||||||
|
-----------------
|
||||||
|
::
|
||||||
|
|
||||||
|
Community => Room (with pointers to Category Rooms)
|
||||||
|
Category => Room
|
||||||
|
Post => Message Events
|
||||||
|
Comment => Message Events (relates_to key)
|
||||||
|
|
||||||
|
Community:
|
||||||
|
- Contains 10s of categories.
|
||||||
|
- Contains special Message Events which point to different rooms f.e Category.
|
||||||
|
- Moderators of the community are mods in this room. They are in charge of making
|
||||||
|
new categories and the subsequent rooms. Can get a bit funky if a mod creates a
|
||||||
|
category room without the same permissions as the community room... but another
|
||||||
|
mod can always delete the pointer to the buggy category room and make a new one.
|
||||||
|
- Do we want to support the idea of forking off new rooms from existing ones? This
|
||||||
|
and forums could benefit from it.
|
||||||
|
|
||||||
|
Category:
|
||||||
|
- Contains 1000s of posts.
|
||||||
|
- Same permissions as the community room. How to enforce? Fork off the community
|
||||||
|
room?
|
||||||
|
|
||||||
|
Posts:
|
||||||
|
- Contains 10s of comments.
|
||||||
|
|
||||||
|
This is similar to forums but you can more reasonably say "screw it, pull in the
|
||||||
|
entire community of posts."
|
||||||
|
|
@ -0,0 +1,14 @@
|
|||||||
|
Matrix Spec Design Workflow
|
||||||
|
===========================
|
||||||
|
|
||||||
|
1. Write use cases
|
||||||
|
|
||||||
|
2. Design data flows for use cases
|
||||||
|
|
||||||
|
3. Design generic API (factoring out commonalities where possible)
|
||||||
|
|
||||||
|
4. Design transport-specific API with justifications
|
||||||
|
|
||||||
|
5. Formalise transport-specific API as swagger or similar
|
||||||
|
|
||||||
|
6. Evolve the generic API design doc and transport-specific API into the actual spec.
|
@ -0,0 +1,408 @@
|
|||||||
|
Federation
|
||||||
|
==========
|
||||||
|
.. sectnum::
|
||||||
|
.. contents:: Table of Contents
|
||||||
|
|
||||||
|
Authorization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
When receiving new events from remote servers, or creating new events, a server
|
||||||
|
must know whether that event is allowed by the authorization rules. These rules
|
||||||
|
depend solely on the state at that event. The types of state events that affect
|
||||||
|
authorization are:
|
||||||
|
|
||||||
|
- ``m.room.create``
|
||||||
|
- ``m.room.member``
|
||||||
|
- ``m.room.join_rules``
|
||||||
|
- ``m.room.power_levels``
|
||||||
|
|
||||||
|
Servers should not create new events that reference unauthorized events.
|
||||||
|
However, any event that does reference an unauthorized event is not itself
|
||||||
|
automatically considered unauthorized.
|
||||||
|
|
||||||
|
Unauthorized events that appear in the event graph do *not* have any effect on
|
||||||
|
the state of the graph.
|
||||||
|
|
||||||
|
.. Note:: This is in contrast to redacted events which can still affect the
|
||||||
|
state of the graph. For example, a redacted *"join"* event will still
|
||||||
|
result in the user being considered joined.
|
||||||
|
|
||||||
|
|
||||||
|
Rules
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
The following are the rules to determine if an event is authorized (this does
|
||||||
|
include validation).
|
||||||
|
|
||||||
|
**TODO**: What signatures do we expect?
|
||||||
|
|
||||||
|
1. If type is ``m.room.create`` allow if and only if it has no prev events.
|
||||||
|
#. If type is ``m.room.member``:
|
||||||
|
|
||||||
|
a. If ``membership`` is ``join``:
|
||||||
|
|
||||||
|
i. If the previous event is an ``m.room.create``, the depth is 1 and
|
||||||
|
the ``state_key`` is the creator, then allow.
|
||||||
|
#. If the ``state_key`` does not match ``sender`` key, reject.
|
||||||
|
#. If the current state has ``membership`` set to ``join``.
|
||||||
|
#. If the ``sender`` is in the ``m.room.may_join`` list. [Not currently
|
||||||
|
implemented]
|
||||||
|
#. If the ``join_rules`` is:
|
||||||
|
|
||||||
|
- ``public``: allow.
|
||||||
|
- ``invite``: allow if the current state has ``membership`` set to
|
||||||
|
``invite``
|
||||||
|
- ``knock``: **TODO**.
|
||||||
|
- ``private``: Reject.
|
||||||
|
|
||||||
|
#. Reject
|
||||||
|
|
||||||
|
#. If ``membership`` is ``invite`` then allow if ``sender`` is in room,
|
||||||
|
otherwise reject.
|
||||||
|
#. If ``membership`` is ``leave``:
|
||||||
|
|
||||||
|
i. If ``sender`` matches ``state_key`` allow.
|
||||||
|
#. If ``sender``'s power level is greater than the the ``kick_level``
|
||||||
|
given in the current ``m.room.power_levels`` state (defaults to 50),
|
||||||
|
and the ``state_key``'s power level is less than or equal to the
|
||||||
|
``sender``'s power level, then allow.
|
||||||
|
#. Reject.
|
||||||
|
|
||||||
|
#. If ``membership`` is ``ban``:
|
||||||
|
|
||||||
|
i. **TODO**.
|
||||||
|
|
||||||
|
#. Reject.
|
||||||
|
|
||||||
|
#. Reject the event if the event type's required power level is less that the
|
||||||
|
``sender``'s power level.
|
||||||
|
#. If the ``sender`` is not in the room, reject.
|
||||||
|
#. If the type is ``m.room.power_levels``:
|
||||||
|
|
||||||
|
a. **TODO**.
|
||||||
|
|
||||||
|
#. Allow.
|
||||||
|
|
||||||
|
|
||||||
|
Definitions
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Required Power Level
|
||||||
|
A given event type has an associated *required power level*. This is given
|
||||||
|
by the current ``m.room.power_levels`` event, it is either listed explicitly
|
||||||
|
in the ``events`` section or given by either ``state_default`` or
|
||||||
|
``events_default`` depending on if the event type is a state event or not.
|
||||||
|
|
||||||
|
|
||||||
|
Auth events
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
The auth events of an event are the set of events used by the authorization
|
||||||
|
algorithm to accept the event. These should be a subset of the current state.
|
||||||
|
|
||||||
|
A server is required to store the complete chain of auth events for all events
|
||||||
|
it serves to remote servers.
|
||||||
|
|
||||||
|
All auth events have type:
|
||||||
|
|
||||||
|
- ``m.room.create``
|
||||||
|
- ``m.room.power_levels``
|
||||||
|
- ``m.room.member``
|
||||||
|
|
||||||
|
.. todo
|
||||||
|
We probably should probably give a lower band of how long auth events
|
||||||
|
should be kept around for.
|
||||||
|
|
||||||
|
Auth chain
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
The *auth chain* for an event is the recursive list of auth events and the auth
|
||||||
|
chain for those auth events.
|
||||||
|
|
||||||
|
.. Note:: The auth chain for an event gives all the information a server needs
|
||||||
|
to accept an event. However, being given an auth chain for an event
|
||||||
|
that appears valid does not mean that the event might not later be
|
||||||
|
rejected. For example if we discover that the sender had been banned
|
||||||
|
between the join event listed in the auth events and the event being
|
||||||
|
authed.
|
||||||
|
|
||||||
|
**TODO**: Clean the above explanations up a bit.
|
||||||
|
|
||||||
|
|
||||||
|
Auth chain resolution
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If an auth check fails, or if we get told something we accepted should have
|
||||||
|
been rejected, we need to try and determine who is right.
|
||||||
|
|
||||||
|
If two servers disagree about the validity of the auth events, both should
|
||||||
|
inform the other of what they think the current auth chain is. If either are
|
||||||
|
missing auth events that they know are valid (through authorization and state
|
||||||
|
resolution) they process the missing events as usual.
|
||||||
|
|
||||||
|
If either side notice that the other has accepted an auth events we think
|
||||||
|
should be rejected (for reasons *not* in their auth chain), that server should
|
||||||
|
inform the other with suitable proof.
|
||||||
|
|
||||||
|
The proofs can be:
|
||||||
|
|
||||||
|
- An *event chain* that shows an auth event is *not* an ancestor of the event.
|
||||||
|
This can be done by giving the full ancestor chains up to the depth of the
|
||||||
|
invalid auth event.
|
||||||
|
- Given an event (and event chain?) showing that authorization had been revoked.
|
||||||
|
|
||||||
|
If a server discovers it cannot prove the other side is wrong, then it accepts
|
||||||
|
that the other is correct; i.e. we always accept that the other side is correct
|
||||||
|
unless we can prove otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
State Resolution
|
||||||
|
----------------
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
When two branches in the event graph merge, the state of those branches might
|
||||||
|
differ, so a *state resolution* algorithm must be used to determine the current
|
||||||
|
state of the resultant merge.
|
||||||
|
|
||||||
|
The properties of the state resolution algorithm are:
|
||||||
|
|
||||||
|
- Must only depend on the event graph, and not local server state.
|
||||||
|
- When two state events are comparable, the descendant one should be picked.
|
||||||
|
- Must not require the full event graph.
|
||||||
|
|
||||||
|
The following algorithm satisfies these requirements; given two or more events,
|
||||||
|
pick the one with the greatest:
|
||||||
|
|
||||||
|
#. Depth.
|
||||||
|
#. Hash of event_id.
|
||||||
|
|
||||||
|
|
||||||
|
This works except in the case of auth events, where we need to mitigate against
|
||||||
|
the attack where servers artificially netsplit to avoid bans or power level
|
||||||
|
changes.
|
||||||
|
|
||||||
|
We want the following rules to apply:
|
||||||
|
|
||||||
|
#. If power levels have been changed on two different branches use the rules
|
||||||
|
above, ensuring that the one picked is a valid change from the one not picked.
|
||||||
|
#. Similarly handle membership changes (e.g. bans, kicks, etc.)
|
||||||
|
#. Any state merged must be allowed by the newly merged auth events. If none of
|
||||||
|
the candidate events for a given state are allowed, we pick the last event
|
||||||
|
given by the ordering above (i.e. we pick one with the least depth).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
State Conflict Resolution
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
If a server discovers that it disagrees with another about the current state,
|
||||||
|
it can follow the same process outlined in *Auth chain resolution* to resolve
|
||||||
|
these conflicts.
|
||||||
|
|
||||||
|
Constructing a new event
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
When constructing a new event, the server should insert the following fields:
|
||||||
|
|
||||||
|
- ``prev_events``: The list of event ids of what the server believes are the
|
||||||
|
current leaf nodes of the event graph (i.e., nodes that have been received
|
||||||
|
but are yet to be referenced by another event).
|
||||||
|
- ``depth``: An integer one greater than the maximum depth of the event's
|
||||||
|
previous events.
|
||||||
|
- ``auth_events``: The list of event ids that authorizes this event. This
|
||||||
|
should be a subset of the current state.
|
||||||
|
- ``origin_server_ts``: The time the server created the event.
|
||||||
|
- ``origin``: The name of the server.
|
||||||
|
|
||||||
|
|
||||||
|
Signing and Hashes
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
Validation
|
||||||
|
----------
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
Domain specific string
|
||||||
|
A string of the form ``<prefix><localpart>:<domain>``, where <prefix> is a
|
||||||
|
single character, ``<localpart>`` is an arbitrary string that does not
|
||||||
|
include a colon, and `<domain>` is a valid server name.
|
||||||
|
|
||||||
|
``room_id``
|
||||||
|
A domain specific string with prefix ``!`` that is static across all events
|
||||||
|
in a graph and uniquely identifies it. The ``domain`` should be that of the
|
||||||
|
home server that created the room (i.e., the server that generated the
|
||||||
|
first ``m.room.create`` event).
|
||||||
|
|
||||||
|
``sender``
|
||||||
|
The entity that logically sent the event. This is usually a user id, but
|
||||||
|
can also be a server name.
|
||||||
|
|
||||||
|
User Id
|
||||||
|
A domain specific string with prefix ``@`` representing a user account. The
|
||||||
|
``domain`` is the home server of the user and is the server used to contact
|
||||||
|
the user.
|
||||||
|
|
||||||
|
Joining a room
|
||||||
|
--------------
|
||||||
|
|
||||||
|
If a user requests to join a room that the server is already in (i.e. the a
|
||||||
|
user on that server has already joined the room) then the server can simply
|
||||||
|
generate a join event and send it as normal.
|
||||||
|
|
||||||
|
If the server is not already in the room it needs to will need to join via
|
||||||
|
another server that is already in the room. This is done as a two step process.
|
||||||
|
|
||||||
|
First, the local server requests from the remote server a skeleton of a join
|
||||||
|
event. The remote does this as the local server does not have the event graph
|
||||||
|
to use to fill out the ``prev_events`` key in the new event. Critically, the
|
||||||
|
remote server does not process the event it responded with.
|
||||||
|
|
||||||
|
Once the local server has this event, it fills it out with any extra data and
|
||||||
|
signs it. Once ready the local server sends this event to a remote server
|
||||||
|
(which could be the same or different from the first remote server), this
|
||||||
|
remote server then processes the event and distributes to all the other
|
||||||
|
participating servers in that room. The local server is told about the
|
||||||
|
current state and complete auth chain for the join event. The local server
|
||||||
|
can then process the join event itself.
|
||||||
|
|
||||||
|
|
||||||
|
.. Note::
|
||||||
|
Finding which server to use to join any particular room is not specified.
|
||||||
|
|
||||||
|
|
||||||
|
Inviting a user
|
||||||
|
---------------
|
||||||
|
|
||||||
|
To invite a remote user to a room we need their home server to sign the invite
|
||||||
|
event. This is done by sending the event to the remote server, which then signs
|
||||||
|
the event, before distributing the invite to other servers.
|
||||||
|
|
||||||
|
|
||||||
|
Handling incoming events
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
When a server receives an event, it should:
|
||||||
|
|
||||||
|
#. Check if it knows about the room. If it doesn't, then it should get the
|
||||||
|
current state and auth events to determine whether the server *should* be in
|
||||||
|
the room. If so continue, if not drop or reject the event
|
||||||
|
#. If the server already knew about the room, check the prev events to see if
|
||||||
|
it is missing any events. If it is, request them. Servers should limit how
|
||||||
|
far back they will walk the event graph for missing events.
|
||||||
|
#. If the server does not have all the prev events, then it should request the
|
||||||
|
current state and auth events from a server.
|
||||||
|
|
||||||
|
|
||||||
|
Failures
|
||||||
|
--------
|
||||||
|
|
||||||
|
A server can notify a remote server about something it thinks it has done
|
||||||
|
wrong using the failures mechanism. For example, the remote accepted an event
|
||||||
|
the local think it shouldn't have.
|
||||||
|
|
||||||
|
A failure has a severity level depending on the action taken by the local
|
||||||
|
server. These levels are:
|
||||||
|
|
||||||
|
``FATAL``
|
||||||
|
The local server could not parse the event, for example due to a missing
|
||||||
|
required field.
|
||||||
|
|
||||||
|
``ERROR``
|
||||||
|
The local server *could* parse the event, but it was rejected. For example,
|
||||||
|
the event may have failed an authorization check.
|
||||||
|
|
||||||
|
``WARN``
|
||||||
|
The local server accepted the event, but something was unexpected about it.
|
||||||
|
For example, the event may have referenced another event the local server
|
||||||
|
thought should be rejected.
|
||||||
|
|
||||||
|
A failure also includes several other fields:
|
||||||
|
|
||||||
|
``code``
|
||||||
|
A numeric code (to be defined later) indicating a particular type of
|
||||||
|
failure.
|
||||||
|
|
||||||
|
``reason``
|
||||||
|
A short string indicating what was wrong, for diagnosis purposes on the
|
||||||
|
remote server.
|
||||||
|
|
||||||
|
``affected``
|
||||||
|
The event id of the event this failure is responding to. For example, if
|
||||||
|
an accepted event referenced a rejected event, this would point to the
|
||||||
|
accepted one.
|
||||||
|
|
||||||
|
``source``
|
||||||
|
The event id of the event that was the source of this unexpected behaviour.
|
||||||
|
For example, if an accepted event referenced a rejected event, this would
|
||||||
|
point to the rejected one.
|
||||||
|
|
||||||
|
|
||||||
|
Appendix
|
||||||
|
========
|
||||||
|
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
Example event:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{
|
||||||
|
"auth_events": [
|
||||||
|
[
|
||||||
|
"$14187571482fLeia:localhost:8480",
|
||||||
|
{
|
||||||
|
"sha256": "kiZUclzzPetHfy0rVoYKnYXnIv5VxH8a4996zVl8xbw"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"$14187571480odWTd:localhost:8480",
|
||||||
|
{
|
||||||
|
"sha256": "GqtndjviW9yPGaZ6EJfzuqVCRg5Lhoyo4YYv1NFP7fw"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"$14205549830rrMar:localhost:8480",
|
||||||
|
{
|
||||||
|
"sha256": "gZmL23QdWjNOmghEZU6YjqgHHrf2fxarKO2z5ZTbkig"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"content": {
|
||||||
|
"body": "Test!",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"depth": 250,
|
||||||
|
"event_id": "$14207181140uTFlx:localhost:8480",
|
||||||
|
"hashes": {
|
||||||
|
"sha256": "k1nuafFdFvZXzhb5NeTE0Q2Jkqu3E8zkh3uH3mqwIxc"
|
||||||
|
},
|
||||||
|
"origin": "localhost:8480",
|
||||||
|
"origin_server_ts": 1420718114694,
|
||||||
|
"prev_events": [
|
||||||
|
[
|
||||||
|
"$142071809077XNNkP:localhost:8480",
|
||||||
|
{
|
||||||
|
"sha256": "xOnU1b+4LOVz5qih0dkNFrdMgUcf35fKx9sdl/gqhjY"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"room_id": "!dwZDafgDEFTtpPKpLy:localhost:8480",
|
||||||
|
"sender": "@bob:localhost:8480",
|
||||||
|
"signatures": {
|
||||||
|
"localhost:8480": {
|
||||||
|
"ed25519:auto": "Nzd3D+emFBJJ4LCTzQEZaKO0Sa3sSTR1fGpu8OWXYn+7XUqke9Q1jYUewrEfxb3lPxlYWm/GztVUJizLz1K5Aw"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
|||||||
|
# JSON Schema for Matrix events. http://json-schema.org/
|
||||||
|
title: "Matrix Event"
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
auth_events:
|
||||||
|
description: The events needed to authenticate this event
|
||||||
|
$ref: "#/definitions/event_reference_list"
|
||||||
|
content:
|
||||||
|
type: object
|
||||||
|
description: The body of the event.
|
||||||
|
depth:
|
||||||
|
type: integer
|
||||||
|
description: A number one greater than that of any preceeding event.
|
||||||
|
minimum: 0
|
||||||
|
event_id:
|
||||||
|
$ref: "#/definitions/event_id"
|
||||||
|
hashes:
|
||||||
|
$ref: "#/definitions/hashes"
|
||||||
|
origin:
|
||||||
|
$ref: "#/definitions/server_id"
|
||||||
|
origin_server_ts:
|
||||||
|
type: integer
|
||||||
|
description: Posix timestamp on the originating server
|
||||||
|
minimum: 0
|
||||||
|
prev_events:
|
||||||
|
description: The event(s) this event came after.
|
||||||
|
$ref: "#/definitions/event_reference_list"
|
||||||
|
prev_state:
|
||||||
|
description: The state event(s) this event replaces.
|
||||||
|
$ref: "#/definitions/event_reference_list"
|
||||||
|
room_id:
|
||||||
|
$ref: "#/definitions/room_id"
|
||||||
|
sender:
|
||||||
|
oneOf:
|
||||||
|
- $ref: "#/definitions/user_id"
|
||||||
|
- $ref: "#/definitions/server_id"
|
||||||
|
state_key:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
unsigned:
|
||||||
|
type: object
|
||||||
|
|
||||||
|
required:
|
||||||
|
- auth_events
|
||||||
|
- content
|
||||||
|
- depth
|
||||||
|
- event_id
|
||||||
|
- hashes
|
||||||
|
- origin
|
||||||
|
- origin_server_ts
|
||||||
|
- prev_events
|
||||||
|
- room_id
|
||||||
|
- sender
|
||||||
|
- type
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
state_key:
|
||||||
|
- prev_state
|
||||||
|
prev_state:
|
||||||
|
- state_key
|
||||||
|
|
||||||
|
definitions:
|
||||||
|
server_id:
|
||||||
|
type: string
|
||||||
|
description: Identifies a server.
|
||||||
|
pattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||||
|
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||||
|
(:[0-9]*[0-9])?$"
|
||||||
|
event_id:
|
||||||
|
type: string
|
||||||
|
description: Identifies an event.
|
||||||
|
pattern: "^\\$[^:]+:\
|
||||||
|
(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||||
|
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||||
|
(:[0-9]*[0-9])?$"
|
||||||
|
room_id:
|
||||||
|
type: string
|
||||||
|
description: Identifies a room.
|
||||||
|
pattern: "^\\![^:]+:\
|
||||||
|
(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||||
|
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||||
|
(:[0-9]*[0-9])?$"
|
||||||
|
user_id:
|
||||||
|
type: string
|
||||||
|
description: Identifies a user.
|
||||||
|
pattern: "^\\@[^:]+:\
|
||||||
|
(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*\
|
||||||
|
([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\
|
||||||
|
(:[0-9]*[0-9])?$"
|
||||||
|
base64:
|
||||||
|
type: string
|
||||||
|
description: Base64 string without padding.
|
||||||
|
pattern: "^[a-zA-Z0-9/+]*$"
|
||||||
|
hashes:
|
||||||
|
type: object
|
||||||
|
description: Hashes
|
||||||
|
minProperties: 1
|
||||||
|
additionalProperties:
|
||||||
|
$ref: "#/definitions/base64"
|
||||||
|
signatures:
|
||||||
|
type: object
|
||||||
|
description: Signatures
|
||||||
|
additionalProperties:
|
||||||
|
type: object
|
||||||
|
minProperties: 1
|
||||||
|
additionalProperties:
|
||||||
|
$ref: "#/definitions/base64"
|
||||||
|
event_reference:
|
||||||
|
type: array
|
||||||
|
minItems: 2
|
||||||
|
maxItems: 2
|
||||||
|
items:
|
||||||
|
- type: string
|
||||||
|
description: Event id of the referenced event.
|
||||||
|
$ref: "#/definitions/event_id"
|
||||||
|
- type: object
|
||||||
|
description: Reference hashes of the referenced event.
|
||||||
|
$ref: "#/definitions/hashes"
|
||||||
|
event_reference_list:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/event_reference"
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
+---------------+
|
||||||
|
| Room |
|
||||||
|
| "Room-ID" |
|
||||||
|
| {State} | +----------------------+
|
||||||
|
| Name------|-------->| Event m.room.name |
|
||||||
|
| Topic | | "Name" |
|
||||||
|
| [Aliases] | +----------------------+ +-------------+
|
||||||
|
| [Members]-|---+ +----------------------+ <----| Start Token |
|
||||||
|
| [Messages] | | | Event m.room.member | +-------------+
|
||||||
|
| | | | +---->| "invite/join/ban" |
|
||||||
|
+---------------+ | "User-ID" |
|
||||||
|
| | +----------------------+
|
||||||
|
| | +----------------------+
|
||||||
|
| | Message | Event m.room.message |
|
||||||
|
| +-------------->| {content} |<--+
|
||||||
|
| +----------------------+ |
|
||||||
|
| Comment +----------------------+ |
|
||||||
|
+------------------>| Event m.room.message | |
|
||||||
|
| {content} | |
|
||||||
|
| "relates-to"-------|---+ +-------------+
|
||||||
|
+----------------------+ <----| End Token |
|
||||||
|
+-------------+
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,317 @@
|
|||||||
|
General UI/UX requirements:
|
||||||
|
===========================
|
||||||
|
- Live updates
|
||||||
|
- No flicker:
|
||||||
|
* Sending message (local echo)
|
||||||
|
* Receiving images (encoding w/h)
|
||||||
|
* Scrollback
|
||||||
|
* Resolving display names (from user ID)
|
||||||
|
- Fast startup times
|
||||||
|
- Fast "opening room" times (esp. when clicking through from a notification)
|
||||||
|
- Low latency file transfer.
|
||||||
|
|
||||||
|
Use cases
|
||||||
|
---------
|
||||||
|
- #1: Lightweight IM client (no perm storage) - e.g. Web client
|
||||||
|
- #2: Bug tracking software
|
||||||
|
- #3: Forum
|
||||||
|
- #4: Google + style communities
|
||||||
|
- #5: Email style threading
|
||||||
|
- #6: Multi-column threaded IM
|
||||||
|
- #7: Mobile IM client (perm storage)
|
||||||
|
- #8: MIDI client
|
||||||
|
- #9: Animatrix client
|
||||||
|
- #10: Unity object trees
|
||||||
|
- #11: Social Network ("Walls", PMs, groups)
|
||||||
|
- #12: Minecraft-clone
|
||||||
|
- #13: Global 'Like' widget, which links through to a room.
|
||||||
|
|
||||||
|
|
||||||
|
#1 Web client UI
|
||||||
|
================
|
||||||
|
|
||||||
|
Model::
|
||||||
|
|
||||||
|
Rooms ----< Messages
|
||||||
|
- name - type (call/image)
|
||||||
|
- topic
|
||||||
|
|
||||||
|
Home Screen
|
||||||
|
What's visible:
|
||||||
|
- Recent chats ordered by timestamp of latest event (with # users)
|
||||||
|
- Your own display name, user ID and avatar url
|
||||||
|
- A searchable list of public rooms (with # users and alias + room name + room topic)
|
||||||
|
What you can do:
|
||||||
|
- Create a room (public/private, with alias)
|
||||||
|
- Join a room from alias
|
||||||
|
- Message a user (with user ID)
|
||||||
|
- Leave a recent room
|
||||||
|
- Open a room
|
||||||
|
- Open a chat history link.
|
||||||
|
- Search for a public room.
|
||||||
|
|
||||||
|
Chat Screen
|
||||||
|
What's visible:
|
||||||
|
- Enough scrollback to fill a "screen full" of content.
|
||||||
|
- Each message: timestamp, user ID, display name at the time the message was
|
||||||
|
sent, avatar URL at the time the message was sent, whether it was a bing message
|
||||||
|
or not.
|
||||||
|
- User list: for each user: presence, current avatar url in the room, current
|
||||||
|
display name in the room, power level, ordered by when they were last speaking.
|
||||||
|
- Recents list: (same as Home Screen)
|
||||||
|
- Room name
|
||||||
|
- Room topic
|
||||||
|
- Typing notifications
|
||||||
|
- Desktop/Push Notifications for messages
|
||||||
|
What you can do:
|
||||||
|
- Invite a user
|
||||||
|
- Kick a user
|
||||||
|
- Ban/Unban a user
|
||||||
|
- Leave the room
|
||||||
|
- Send a message (image/text/emote)
|
||||||
|
- Change someone's power level
|
||||||
|
- Change your own display name
|
||||||
|
- Accept an incoming call
|
||||||
|
- Make an outgoing call
|
||||||
|
- Get older messages by scrolling up (scrollback)
|
||||||
|
- Redact a message
|
||||||
|
- Resend a message which was not sent
|
||||||
|
Message sending:
|
||||||
|
- Immediate local echo
|
||||||
|
- Queue up messages which haven't been sent yet
|
||||||
|
- Reordering local echo to where it actually happened
|
||||||
|
VoIP:
|
||||||
|
- One entry in your display for a call (which may contain duration, type, status)
|
||||||
|
- Glare resolution
|
||||||
|
Scrollback:
|
||||||
|
- Display in reverse chronological order by the originating server's timestamp
|
||||||
|
- Terminates at the start of the room (which then makes it impossible to request
|
||||||
|
more scrollback)
|
||||||
|
Local storage:
|
||||||
|
- Driven by desire for fast startup times and minimal network traffic
|
||||||
|
- Display messages from storage and from the network without any gaps in messages.
|
||||||
|
- Persist scrollback if possible: Scrollback from storage first then from the
|
||||||
|
network.
|
||||||
|
Notifications:
|
||||||
|
- Receive notifications for rooms you're interested in (explicitly or from a default)
|
||||||
|
- Maybe per device.
|
||||||
|
- Maybe depending on presence (e.g. idle)
|
||||||
|
- Maybe depending on message volume
|
||||||
|
- Maybe depending on room config options.
|
||||||
|
Message contents:
|
||||||
|
- images
|
||||||
|
- video
|
||||||
|
- rich text
|
||||||
|
- audio
|
||||||
|
- arbitrary files
|
||||||
|
- location
|
||||||
|
- vcards (potentially)
|
||||||
|
|
||||||
|
Chat History Screen
|
||||||
|
What's visible:
|
||||||
|
- The linked message and enough scrollback to fill a "screen full" of content.
|
||||||
|
- Each message: timestamp, user ID, display name at the time the message was
|
||||||
|
sent, avatar URL at the time the message was sent, whether it was a bing message
|
||||||
|
or not.
|
||||||
|
- The historical user list. *TODO: Is this taken at the linked message, or at
|
||||||
|
wherever the user has scrolled to?*
|
||||||
|
What you can do:
|
||||||
|
- Get older messages by scrolling up (scrollback)
|
||||||
|
- Get newer messages by scrolling down
|
||||||
|
|
||||||
|
Public Room Search Screen
|
||||||
|
What's visible:
|
||||||
|
- The current search text.
|
||||||
|
- The homeserver being searched (defaults to the HS the client is connected to).
|
||||||
|
- The results of the current search with enough results to fill the screen
|
||||||
|
with # users and alias + room name + room topic.
|
||||||
|
What you can do:
|
||||||
|
- Change what you are searching for.
|
||||||
|
- Change the server that's being searched.
|
||||||
|
- Scroll down to get more search results.
|
||||||
|
|
||||||
|
User screen
|
||||||
|
What's visible:
|
||||||
|
- Display name
|
||||||
|
- Avatar
|
||||||
|
- User ID
|
||||||
|
What you can do:
|
||||||
|
- Start a chat with the user
|
||||||
|
|
||||||
|
|
||||||
|
#2 Bug tracking UI
|
||||||
|
==================
|
||||||
|
|
||||||
|
Model::
|
||||||
|
|
||||||
|
Projects ----< Issues ---< Comments
|
||||||
|
- key - summary - user
|
||||||
|
- name - ID - message
|
||||||
|
SYN SYN-52 Fix it nooow!
|
||||||
|
|
||||||
|
Landing page
|
||||||
|
What's visible:
|
||||||
|
- Issues assigned to me
|
||||||
|
- Issues I'm watching
|
||||||
|
- Recent activity on other issues (not refined to me)
|
||||||
|
- List of projects
|
||||||
|
What you can do:
|
||||||
|
- View an issue
|
||||||
|
- Create an issue
|
||||||
|
- Sort issues
|
||||||
|
- View a user
|
||||||
|
- View a project
|
||||||
|
- Search for issues (by name, time, priority, description contents, reporter, etc...)
|
||||||
|
|
||||||
|
Issue page
|
||||||
|
What's visible:
|
||||||
|
- Summary of issue
|
||||||
|
- Issue key
|
||||||
|
- Project affected
|
||||||
|
- Description
|
||||||
|
- Comments
|
||||||
|
- Priority, labels, type, purpose, etc..
|
||||||
|
- Reporter/assignee
|
||||||
|
- Creation and last updated times
|
||||||
|
- History of issue changes
|
||||||
|
What you can do:
|
||||||
|
- Comment on issue
|
||||||
|
- Change issue info (labels, type, purpose, etc..)
|
||||||
|
- Open/Close/Resolve the issue
|
||||||
|
- Edit the issue
|
||||||
|
- Watch/Unwatch the issue
|
||||||
|
|
||||||
|
|
||||||
|
#3 Forum UI
|
||||||
|
===========
|
||||||
|
|
||||||
|
Model::
|
||||||
|
|
||||||
|
Forum ----< Boards ----< Threads ----< Messages
|
||||||
|
- Matrix - Dev - HALP! - please halp!
|
||||||
|
|
||||||
|
Main page
|
||||||
|
What's visible:
|
||||||
|
- Categories (containing boards)
|
||||||
|
- Boards (with names and # posts and tagline and latest post)
|
||||||
|
What you can do:
|
||||||
|
- View a board
|
||||||
|
- View the latest message on a board
|
||||||
|
|
||||||
|
Board page
|
||||||
|
What's visible:
|
||||||
|
- Threads (titles, OP, latest post date+author, # replies, # upvotes, whether
|
||||||
|
the OP contains an image or hyperlink (small icon on title))
|
||||||
|
- Whether the thread is answered (with link to the answer)
|
||||||
|
- Pagination for posts within a thread (1,2,3,4,5...10)
|
||||||
|
- Pagination for threads within a board
|
||||||
|
- List of threads in chronological order
|
||||||
|
- Stickied threads
|
||||||
|
What you can do:
|
||||||
|
- View a user
|
||||||
|
- View a thread on a particular page
|
||||||
|
- View the latest message on a thread
|
||||||
|
- View older threads (pagination)
|
||||||
|
- Search the board
|
||||||
|
|
||||||
|
Thread page
|
||||||
|
What's visible:
|
||||||
|
- Messages in chronological order
|
||||||
|
- For each message: author, timestamp, # posts by author, avatar, registration
|
||||||
|
date, status message, message contents, # views of message
|
||||||
|
What you can do:
|
||||||
|
- Upvote the message
|
||||||
|
- Flag the message for a mod
|
||||||
|
- Reply to the message
|
||||||
|
- Subscribe to thread or message's RSS feed
|
||||||
|
- Go to previous/next thread
|
||||||
|
|
||||||
|
|
||||||
|
#4 Google+ community
|
||||||
|
====================
|
||||||
|
|
||||||
|
Model::
|
||||||
|
|
||||||
|
Community -----< Categories ----< Posts ---< Comments
|
||||||
|
Kerbal SP Mods, Help Text Text
|
||||||
|
(no title!)
|
||||||
|
|
||||||
|
Communities page
|
||||||
|
What's visible:
|
||||||
|
- List of communities
|
||||||
|
- For each community: # users, # posts, group pic, title
|
||||||
|
What you can do:
|
||||||
|
- Join a community
|
||||||
|
- View a community
|
||||||
|
|
||||||
|
Community Page
|
||||||
|
What's visible:
|
||||||
|
- Title, pic
|
||||||
|
- List of categories
|
||||||
|
- List of members with avatars (+ total #)
|
||||||
|
- Most recent posts with comments (most recent comment if >1)
|
||||||
|
What you can do:
|
||||||
|
- Join the group
|
||||||
|
- Post a post (with voting and options)
|
||||||
|
- Report abuse
|
||||||
|
- View member
|
||||||
|
- Expand comments
|
||||||
|
- Infinite scrolling
|
||||||
|
- Add a comment to a post
|
||||||
|
- Share a post
|
||||||
|
- +1 a post
|
||||||
|
|
||||||
|
#5 Email style threading
|
||||||
|
========================
|
||||||
|
|
||||||
|
Chat Screen
|
||||||
|
What's visible:
|
||||||
|
- Enough scrollback to fill a "screen full" of content.
|
||||||
|
- Threads:
|
||||||
|
|
||||||
|
- Initially will only display the timestamp and user ID of the *first*
|
||||||
|
message. But can expand to show the entire tree.
|
||||||
|
- Tree of messages indicating which message is a reply to which.
|
||||||
|
- Ordered by the arbitrary field (timestamp of oldest message in thread;
|
||||||
|
newest message in thread; sender id; sender display name; etc)
|
||||||
|
- Each message: timestamp, user ID, display name at the time of the message
|
||||||
|
- Room name
|
||||||
|
- Room topic
|
||||||
|
- Typing notifications
|
||||||
|
- Desktop/Push Notifications for messages
|
||||||
|
What you can do:
|
||||||
|
- Send a message in reply to another message:
|
||||||
|
|
||||||
|
- Immediate local echo, may cause messages to re-order
|
||||||
|
- Messages that haven't reached the server are queued.
|
||||||
|
- Thread is displayed where it should be in the thread order once the
|
||||||
|
message is sent.
|
||||||
|
- Start a new thread by sending a message.
|
||||||
|
|
||||||
|
#6 Multi-threaded IM
|
||||||
|
====================
|
||||||
|
|
||||||
|
Chat Screen
|
||||||
|
What's visible:
|
||||||
|
- A multi-column grid of threads from a number of chatrooms
|
||||||
|
Each concurrent thread is displayed in a different column.
|
||||||
|
The columns start and end as threads split and rejoin the main conversation
|
||||||
|
The messages for each thread are ordered by how recent they are::
|
||||||
|
|
||||||
|
Room #1 Room # 2 Room # 2
|
||||||
|
+------------+ +----------------+ Side thread.
|
||||||
|
| * Message1 | | * Root | +--------------+
|
||||||
|
| * Message2 | | * A1 -> Root | | * B1 -> Root |
|
||||||
|
+------------+ | * A2 -> A1 | | * B2 -> B1 |
|
||||||
|
| * M -> A2, B2 | +--------------+
|
||||||
|
+----------------+
|
||||||
|
|
||||||
|
- Typing notifications. Displayed within the correct thread/column.
|
||||||
|
|
||||||
|
What you can do:
|
||||||
|
- Send a message into a particular thread/column.
|
||||||
|
- Move an *existing* message into a new thread creating a new column
|
||||||
|
- Move an existing message into an existing thread, causing the threads to
|
||||||
|
reconverge (i.e. provide a route from the sidebar back into the existing
|
||||||
|
thread). This does not imply terminating the thread, which can continue
|
||||||
|
independently of the merge.
|
@ -0,0 +1,17 @@
|
|||||||
|
.loggedin {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
{
|
||||||
|
border-spacing:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,td
|
||||||
|
{
|
||||||
|
padding:5px;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<div>
|
||||||
|
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p>
|
||||||
|
</div>
|
||||||
|
<form class="loginForm">
|
||||||
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||||
|
<input type="button" class="login" value="Login"></input>
|
||||||
|
</form>
|
||||||
|
<div class="loggedin">
|
||||||
|
<form class="createRoomForm">
|
||||||
|
<input type="text" id="roomAlias" placeholder="Room alias (optional)"></input>
|
||||||
|
<input type="button" class="createRoom" value="Create Room"></input>
|
||||||
|
</form>
|
||||||
|
<form class="sendMessageForm">
|
||||||
|
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||||
|
<input type="text" id="messageBody" placeholder="Message body"></input>
|
||||||
|
<input type="button" class="sendMessage" value="Send Message"></input>
|
||||||
|
</form>
|
||||||
|
<table id="rooms">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Room ID</th>
|
||||||
|
<th>My state</th>
|
||||||
|
<th>Room Alias</th>
|
||||||
|
<th>Latest message</th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,113 @@
|
|||||||
|
var accountInfo = {};
|
||||||
|
|
||||||
|
var showLoggedIn = function(data) {
|
||||||
|
accountInfo = data;
|
||||||
|
getCurrentRoomList();
|
||||||
|
$(".loggedin").css({visibility: "visible"});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.login').live('click', function() {
|
||||||
|
var user = $("#userLogin").val();
|
||||||
|
var password = $("#passwordLogin").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
showLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var getCurrentRoomList = function() {
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
var rooms = data.rooms;
|
||||||
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
|
addRoom(rooms[i]);
|
||||||
|
}
|
||||||
|
}).fail(function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.createRoom').live('click', function() {
|
||||||
|
var roomAlias = $("#roomAlias").val();
|
||||||
|
var data = {};
|
||||||
|
if (roomAlias.length > 0) {
|
||||||
|
data.room_alias_name = roomAlias;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
data.membership = "join"; // you are automatically joined into every room you make.
|
||||||
|
data.latest_message = "";
|
||||||
|
addRoom(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var addRoom = function(data) {
|
||||||
|
row = "<tr>" +
|
||||||
|
"<td>"+data.room_id+"</td>" +
|
||||||
|
"<td>"+data.membership+"</td>" +
|
||||||
|
"<td>"+data.room_alias+"</td>" +
|
||||||
|
"<td>"+data.latest_message+"</td>" +
|
||||||
|
"</tr>";
|
||||||
|
$("#rooms").append(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.sendMessage').live('click', function() {
|
||||||
|
var roomId = $("#roomId").val();
|
||||||
|
var body = $("#messageBody").val();
|
||||||
|
var msgId = $.now();
|
||||||
|
|
||||||
|
if (roomId.length === 0 || body.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: body
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
$("#messageBody").val("");
|
||||||
|
// wipe the table and reload it. Using the event stream would be the best
|
||||||
|
// solution but that is out of scope of this fiddle.
|
||||||
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
getCurrentRoomList();
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
.loggedin {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
{
|
||||||
|
border-spacing:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,td
|
||||||
|
{
|
||||||
|
padding:5px;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<div>
|
||||||
|
<p>This event stream demo requires a home server to be running on http://localhost:8008</p>
|
||||||
|
</div>
|
||||||
|
<form class="loginForm">
|
||||||
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||||
|
<input type="button" class="login" value="Login"></input>
|
||||||
|
</form>
|
||||||
|
<div class="loggedin">
|
||||||
|
<form class="sendMessageForm">
|
||||||
|
<input type="button" class="sendMessage" value="Send random message"></input>
|
||||||
|
</form>
|
||||||
|
<p id="streamErrorText"></p>
|
||||||
|
<table id="rooms">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Room ID</th>
|
||||||
|
<th>Latest message</th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,145 @@
|
|||||||
|
var accountInfo = {};
|
||||||
|
|
||||||
|
var eventStreamInfo = {
|
||||||
|
from: "END"
|
||||||
|
};
|
||||||
|
|
||||||
|
var roomInfo = [];
|
||||||
|
|
||||||
|
var longpollEventStream = function() {
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$from", eventStreamInfo.from);
|
||||||
|
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
eventStreamInfo.from = data.end;
|
||||||
|
|
||||||
|
var hasNewLatestMessage = false;
|
||||||
|
for (var i=0; i<data.chunk.length; ++i) {
|
||||||
|
if (data.chunk[i].type === "m.room.message") {
|
||||||
|
for (var j=0; j<roomInfo.length; ++j) {
|
||||||
|
if (roomInfo[j].room_id === data.chunk[i].room_id) {
|
||||||
|
roomInfo[j].latest_message = data.chunk[i].content.body;
|
||||||
|
hasNewLatestMessage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNewLatestMessage) {
|
||||||
|
setRooms(roomInfo);
|
||||||
|
}
|
||||||
|
$("#streamErrorText").text("");
|
||||||
|
longpollEventStream();
|
||||||
|
}).fail(function(err) {
|
||||||
|
$("#streamErrorText").text("Event stream error: "+JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
setTimeout(longpollEventStream, 5000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var showLoggedIn = function(data) {
|
||||||
|
accountInfo = data;
|
||||||
|
longpollEventStream();
|
||||||
|
getCurrentRoomList();
|
||||||
|
$(".loggedin").css({visibility: "visible"});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.login').live('click', function() {
|
||||||
|
var user = $("#userLogin").val();
|
||||||
|
var password = $("#passwordLogin").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
showLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var getCurrentRoomList = function() {
|
||||||
|
$("#roomId").val("");
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
var rooms = data.rooms;
|
||||||
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
|
if ("messages" in rooms[i]) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomInfo = rooms;
|
||||||
|
setRooms(roomInfo);
|
||||||
|
}).fail(function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.sendMessage').live('click', function() {
|
||||||
|
if (roomInfo.length === 0) {
|
||||||
|
alert("There is no room to send a message to!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = Math.floor(Math.random() * roomInfo.length);
|
||||||
|
|
||||||
|
sendMessage(roomInfo[index].room_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var sendMessage = function(roomId) {
|
||||||
|
var body = "jsfiddle message @" + $.now();
|
||||||
|
|
||||||
|
if (roomId.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: body
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
$("#messageBody").val("");
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var setRooms = function(roomList) {
|
||||||
|
// wipe existing entries
|
||||||
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
|
||||||
|
var rows = "";
|
||||||
|
for (var i=0; i<roomList.length; ++i) {
|
||||||
|
row = "<tr>" +
|
||||||
|
"<td>"+roomList[i].room_id+"</td>" +
|
||||||
|
"<td>"+roomList[i].latest_message+"</td>" +
|
||||||
|
"</tr>";
|
||||||
|
rows += row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#rooms").append(rows);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
|||||||
|
.roomListDashboard, .roomContents, .sendMessageForm {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomList {
|
||||||
|
background-color: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageWrapper {
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
height: 400px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membersWrapper {
|
||||||
|
background-color: #EEEEEE;
|
||||||
|
height: 200px;
|
||||||
|
width: 50%;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textEntry {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
{
|
||||||
|
border-spacing:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,td
|
||||||
|
{
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomList tr:not(:first-child):hover {
|
||||||
|
background-color: orange;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
name: Example Matrix Client
|
||||||
|
description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
|
||||||
|
authors:
|
||||||
|
- matrix.org
|
||||||
|
resources:
|
||||||
|
- http://matrix.org
|
||||||
|
normalize_css: no
|
@ -0,0 +1,56 @@
|
|||||||
|
<div class="signUp">
|
||||||
|
<p>Matrix example application: Requires a local home server running at http://localhost:8008</p>
|
||||||
|
<form class="registrationForm">
|
||||||
|
<p>No account? Register:</p>
|
||||||
|
<input type="text" id="userReg" placeholder="Username"></input>
|
||||||
|
<input type="password" id="passwordReg" placeholder="Password"></input>
|
||||||
|
<input type="button" class="register" value="Register"></input>
|
||||||
|
</form>
|
||||||
|
<form class="loginForm">
|
||||||
|
<p>Got an account? Login:</p>
|
||||||
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||||
|
<input type="button" class="login" value="Login"></input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roomListDashboard">
|
||||||
|
<form class="createRoomForm">
|
||||||
|
<input type="text" id="roomAlias" placeholder="Room alias"></input>
|
||||||
|
<input type="button" class="createRoom" value="Create Room"></input>
|
||||||
|
</form>
|
||||||
|
<table id="rooms" class="roomList">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Room</th>
|
||||||
|
<th>My state</th>
|
||||||
|
<th>Latest message</th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roomContents">
|
||||||
|
<p id="roomName">Select a room</p>
|
||||||
|
<div class="messageWrapper">
|
||||||
|
<table id="messages">
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<form class="sendMessageForm">
|
||||||
|
<input type="text" class="textEntry" id="body" placeholder="Enter text here..." onkeydown="javascript:if (event.keyCode == 13) document.getElementById('sendMsg').focus()"></input>
|
||||||
|
<input type="button" class="sendMessage" id="sendMsg" value="Send"></input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Member list:</p>
|
||||||
|
<div class="membersWrapper">
|
||||||
|
<table id="members">
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,327 @@
|
|||||||
|
var accountInfo = {};
|
||||||
|
|
||||||
|
var eventStreamInfo = {
|
||||||
|
from: "END"
|
||||||
|
};
|
||||||
|
|
||||||
|
var roomInfo = [];
|
||||||
|
var memberInfo = [];
|
||||||
|
var viewingRoomId;
|
||||||
|
|
||||||
|
// ************** Event Streaming **************
|
||||||
|
var longpollEventStream = function() {
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$from", eventStreamInfo.from);
|
||||||
|
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
eventStreamInfo.from = data.end;
|
||||||
|
|
||||||
|
var hasNewLatestMessage = false;
|
||||||
|
var updatedMemberList = false;
|
||||||
|
var i=0;
|
||||||
|
var j=0;
|
||||||
|
for (i=0; i<data.chunk.length; ++i) {
|
||||||
|
if (data.chunk[i].type === "m.room.message") {
|
||||||
|
console.log("Got new message: " + JSON.stringify(data.chunk[i]));
|
||||||
|
if (viewingRoomId === data.chunk[i].room_id) {
|
||||||
|
addMessage(data.chunk[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (j=0; j<roomInfo.length; ++j) {
|
||||||
|
if (roomInfo[j].room_id === data.chunk[i].room_id) {
|
||||||
|
roomInfo[j].latest_message = data.chunk[i].content.body;
|
||||||
|
hasNewLatestMessage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (data.chunk[i].type === "m.room.member") {
|
||||||
|
if (viewingRoomId === data.chunk[i].room_id) {
|
||||||
|
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
||||||
|
addMessage(data.chunk[i]);
|
||||||
|
for (j=0; j<memberInfo.length; ++j) {
|
||||||
|
if (memberInfo[j].state_key === data.chunk[i].state_key) {
|
||||||
|
memberInfo[j] = data.chunk[i];
|
||||||
|
updatedMemberList = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!updatedMemberList) {
|
||||||
|
memberInfo.push(data.chunk[i]);
|
||||||
|
updatedMemberList = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.chunk[i].state_key === accountInfo.user_id) {
|
||||||
|
getCurrentRoomList(); // update our join/invite list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Discarding: " + JSON.stringify(data.chunk[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNewLatestMessage) {
|
||||||
|
setRooms(roomInfo);
|
||||||
|
}
|
||||||
|
if (updatedMemberList) {
|
||||||
|
$("#members").empty();
|
||||||
|
for (i=0; i<memberInfo.length; ++i) {
|
||||||
|
addMember(memberInfo[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
longpollEventStream();
|
||||||
|
}).fail(function(err) {
|
||||||
|
setTimeout(longpollEventStream, 5000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ************** Registration and Login **************
|
||||||
|
var onLoggedIn = function(data) {
|
||||||
|
accountInfo = data;
|
||||||
|
longpollEventStream();
|
||||||
|
getCurrentRoomList();
|
||||||
|
$(".roomListDashboard").css({visibility: "visible"});
|
||||||
|
$(".roomContents").css({visibility: "visible"});
|
||||||
|
$(".signUp").css({display: "none"});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.login').live('click', function() {
|
||||||
|
var user = $("#userLogin").val();
|
||||||
|
var password = $("#passwordLogin").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
onLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert("Unable to login: is the home server running?");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.register').live('click', function() {
|
||||||
|
var user = $("#userReg").val();
|
||||||
|
var password = $("#passwordReg").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
onLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var msg = "Is the home server running?";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson !== null) {
|
||||||
|
msg = errJson.error;
|
||||||
|
}
|
||||||
|
alert("Unable to register: "+msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ************** Creating a room ******************
|
||||||
|
$('.createRoom').live('click', function() {
|
||||||
|
var roomAlias = $("#roomAlias").val();
|
||||||
|
var data = {};
|
||||||
|
if (roomAlias.length > 0) {
|
||||||
|
data.room_alias_name = roomAlias;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(response) {
|
||||||
|
$("#roomAlias").val("");
|
||||||
|
response.membership = "join"; // you are automatically joined into every room you make.
|
||||||
|
response.latest_message = "";
|
||||||
|
|
||||||
|
roomInfo.push(response);
|
||||||
|
setRooms(roomInfo);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ************** Getting current state **************
|
||||||
|
var getCurrentRoomList = function() {
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
var rooms = data.rooms;
|
||||||
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
|
if ("messages" in rooms[i]) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomInfo = rooms;
|
||||||
|
setRooms(roomInfo);
|
||||||
|
}).fail(function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var loadRoomContent = function(roomId) {
|
||||||
|
console.log("loadRoomContent " + roomId);
|
||||||
|
viewingRoomId = roomId;
|
||||||
|
$("#roomName").text("Room: "+roomId);
|
||||||
|
$(".sendMessageForm").css({visibility: "visible"});
|
||||||
|
getMessages(roomId);
|
||||||
|
getMemberList(roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
var getMessages = function(roomId) {
|
||||||
|
$("#messages").empty();
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
|
||||||
|
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
for (var i=data.chunk.length-1; i>=0; --i) {
|
||||||
|
addMessage(data.chunk[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getMemberList = function(roomId) {
|
||||||
|
$("#members").empty();
|
||||||
|
memberInfo = [];
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
|
||||||
|
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
for (var i=0; i<data.chunk.length; ++i) {
|
||||||
|
memberInfo.push(data.chunk[i]);
|
||||||
|
addMember(data.chunk[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ************** Sending messages **************
|
||||||
|
$('.sendMessage').live('click', function() {
|
||||||
|
if (viewingRoomId === undefined) {
|
||||||
|
alert("There is no room to send a message to!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var body = $("#body").val();
|
||||||
|
sendMessage(viewingRoomId, body);
|
||||||
|
});
|
||||||
|
|
||||||
|
var sendMessage = function(roomId, body) {
|
||||||
|
var msgId = $.now();
|
||||||
|
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: body
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
$("#body").val("");
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ************** Navigation and DOM manipulation **************
|
||||||
|
var setRooms = function(roomList) {
|
||||||
|
// wipe existing entries
|
||||||
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
|
||||||
|
var rows = "";
|
||||||
|
for (var i=0; i<roomList.length; ++i) {
|
||||||
|
row = "<tr>" +
|
||||||
|
"<td>"+roomList[i].room_id+"</td>" +
|
||||||
|
"<td>"+roomList[i].membership+"</td>" +
|
||||||
|
"<td>"+roomList[i].latest_message+"</td>" +
|
||||||
|
"</tr>";
|
||||||
|
rows += row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#rooms").append(rows);
|
||||||
|
|
||||||
|
$('#rooms').find("tr").click(function(){
|
||||||
|
var roomId = $(this).find('td:eq(0)').text();
|
||||||
|
var membership = $(this).find('td:eq(1)').text();
|
||||||
|
if (membership !== "join") {
|
||||||
|
console.log("Joining room " + roomId);
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({membership: "join"}),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
loadRoomContent(roomId);
|
||||||
|
getCurrentRoomList();
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
loadRoomContent(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var addMessage = function(data) {
|
||||||
|
|
||||||
|
var msg = data.content.body;
|
||||||
|
if (data.type === "m.room.member") {
|
||||||
|
if (data.content.membership === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.content.membership === "invite") {
|
||||||
|
msg = "<em>invited " + data.state_key + " to the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "join") {
|
||||||
|
msg = "<em>joined the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "leave") {
|
||||||
|
msg = "<em>left the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "ban") {
|
||||||
|
msg = "<em>was banned from the room</em>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = "<tr>" +
|
||||||
|
"<td>"+data.user_id+"</td>" +
|
||||||
|
"<td>"+msg+"</td>" +
|
||||||
|
"</tr>";
|
||||||
|
$("#messages").append(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
var addMember = function(data) {
|
||||||
|
var row = "<tr>" +
|
||||||
|
"<td>"+data.state_key+"</td>" +
|
||||||
|
"<td>"+data.content.membership+"</td>" +
|
||||||
|
"</tr>";
|
||||||
|
$("#members").append(row);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
.loggedin {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<div>
|
||||||
|
<p>This registration/login demo requires a home server to be running on http://localhost:8008</p>
|
||||||
|
</div>
|
||||||
|
<form class="registrationForm">
|
||||||
|
<input type="text" id="user" placeholder="Username"></input>
|
||||||
|
<input type="password" id="password" placeholder="Password"></input>
|
||||||
|
<input type="button" class="register" value="Register"></input>
|
||||||
|
</form>
|
||||||
|
<form class="loginForm">
|
||||||
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||||
|
<input type="button" class="login" value="Login"></input>
|
||||||
|
</form>
|
||||||
|
<div class="loggedin">
|
||||||
|
<p id="welcomeText"></p>
|
||||||
|
<input type="button" class="testToken" value="Test token"></input>
|
||||||
|
<input type="button" class="logout" value="Logout"></input>
|
||||||
|
<p id="imSyncText"></p>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,79 @@
|
|||||||
|
var accountInfo = {};
|
||||||
|
|
||||||
|
var showLoggedIn = function(data) {
|
||||||
|
accountInfo = data;
|
||||||
|
$(".loggedin").css({visibility: "visible"});
|
||||||
|
$("#welcomeText").text("Welcome " + accountInfo.user_id+". Your access token is: " +
|
||||||
|
accountInfo.access_token);
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.register').live('click', function() {
|
||||||
|
var user = $("#user").val();
|
||||||
|
var password = $("#password").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
showLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var login = function(user, password) {
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
showLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.login').live('click', function() {
|
||||||
|
var user = $("#userLogin").val();
|
||||||
|
var password = $("#passwordLogin").val();
|
||||||
|
$.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) {
|
||||||
|
if (data.flows[0].type !== "m.login.password") {
|
||||||
|
alert("I don't know how to login with this type: " + data.type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
login(user, password);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.logout').live('click', function() {
|
||||||
|
accountInfo = {};
|
||||||
|
$("#imSyncText").text("");
|
||||||
|
$(".loggedin").css({visibility: "hidden"});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.testToken').live('click', function() {
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
||||||
|
}).fail(function(err) {
|
||||||
|
$("#imSyncText").text(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
.loggedin {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
{
|
||||||
|
border-spacing:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,td
|
||||||
|
{
|
||||||
|
padding:5px;
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<div>
|
||||||
|
<p>This room membership demo requires a home server to be running on http://localhost:8008</p>
|
||||||
|
</div>
|
||||||
|
<form class="loginForm">
|
||||||
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||||
|
<input type="button" class="login" value="Login"></input>
|
||||||
|
</form>
|
||||||
|
<div class="loggedin">
|
||||||
|
<form class="createRoomForm">
|
||||||
|
<input type="button" class="createRoom" value="Create Room"></input>
|
||||||
|
</form>
|
||||||
|
<form class="changeMembershipForm">
|
||||||
|
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||||
|
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
||||||
|
<select id="membership">
|
||||||
|
<option value="invite">invite</option>
|
||||||
|
<option value="join">join</option>
|
||||||
|
<option value="leave">leave</option>
|
||||||
|
</select>
|
||||||
|
<input type="button" class="changeMembership" value="Change Membership"></input>
|
||||||
|
</form>
|
||||||
|
<form class="joinAliasForm">
|
||||||
|
<input type="text" id="roomAlias" placeholder="Room Alias (#name:domain)"></input>
|
||||||
|
<input type="button" class="joinAlias" value="Join via Alias"></input>
|
||||||
|
</form>
|
||||||
|
<table id="rooms">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Room ID</th>
|
||||||
|
<th>My state</th>
|
||||||
|
<th>Room Alias</th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,141 @@
|
|||||||
|
var accountInfo = {};
|
||||||
|
|
||||||
|
var showLoggedIn = function(data) {
|
||||||
|
accountInfo = data;
|
||||||
|
getCurrentRoomList();
|
||||||
|
$(".loggedin").css({visibility: "visible"});
|
||||||
|
$("#membership").change(function() {
|
||||||
|
if ($("#membership").val() === "invite") {
|
||||||
|
$("#targetUser").css({visibility: "visible"});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("#targetUser").css({visibility: "hidden"});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.login').live('click', function() {
|
||||||
|
var user = $("#userLogin").val();
|
||||||
|
var password = $("#passwordLogin").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
showLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var getCurrentRoomList = function() {
|
||||||
|
$("#roomId").val("");
|
||||||
|
// wipe the table and reload it. Using the event stream would be the best
|
||||||
|
// solution but that is out of scope of this fiddle.
|
||||||
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
|
$.getJSON(url, function(data) {
|
||||||
|
var rooms = data.rooms;
|
||||||
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
|
addRoom(rooms[i]);
|
||||||
|
}
|
||||||
|
}).fail(function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.createRoom').live('click', function() {
|
||||||
|
var data = {};
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
data.membership = "join"; // you are automatically joined into every room you make.
|
||||||
|
data.latest_message = "";
|
||||||
|
addRoom(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var addRoom = function(data) {
|
||||||
|
row = "<tr>" +
|
||||||
|
"<td>"+data.room_id+"</td>" +
|
||||||
|
"<td>"+data.membership+"</td>" +
|
||||||
|
"<td>"+data.room_alias+"</td>" +
|
||||||
|
"</tr>";
|
||||||
|
$("#rooms").append(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.changeMembership').live('click', function() {
|
||||||
|
var roomId = $("#roomId").val();
|
||||||
|
var member = $("#targetUser").val();
|
||||||
|
var membership = $("#membership").val();
|
||||||
|
|
||||||
|
if (roomId.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
|
url = url.replace("$membership", membership);
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
if (membership === "invite") {
|
||||||
|
data = {
|
||||||
|
user_id: member
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
getCurrentRoomList();
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.joinAlias').live('click', function() {
|
||||||
|
var roomAlias = $("#roomAlias").val();
|
||||||
|
var url = "http://localhost:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token";
|
||||||
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
|
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
getCurrentRoomList();
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue