our
$VERSION
=
'0.19'
;
$VERSION
=
eval
$VERSION
;
*Member
= \
&Net::Async::Matrix::Room::State::Member
;
use
constant
TYPING_RESEND_SECONDS
=> 30;
sub
_init
{
my
$self
=
shift
;
my
(
$params
) =
@_
;
$self
->SUPER::_init(
$params
);
$self
->{matrix} =
delete
$params
->{matrix};
$self
->{room_id} =
delete
$params
->{room_id};
$self
->{typing_members} = {};
$self
->{live_state} = Net::Async::Matrix::Room::State->new(
$self
);
}
sub
configure
{
my
$self
=
shift
;
my
%params
=
@_
;
foreach
(
qw( on_message on_back_message on_membership on_back_membership
on_presence on_synced_state on_state_changed on_back_state_changed
on_typing on_members_typing on_read_receipt )
) {
$self
->{
$_
} =
delete
$params
{
$_
}
if
exists
$params
{
$_
};
}
$self
->SUPER::configure(
%params
);
}
sub
_delete_null_changes
{
my
(
$changes
) =
@_
;
foreach
(
keys
%$changes
) {
my
(
$old
,
$new
) = @{
$changes
->{
$_
} };
delete
$changes
->{
$_
}
if
!
defined
$old
and !
defined
$new
or
defined
$old
and
defined
$new
and
$old
eq
$new
;
}
}
sub
_pushdown_changes
{
my
(
$ch
) =
@_
;
my
(
$oldhash
,
$newhash
) =
@$ch
;
my
%changes
;
foreach
(
keys
%$oldhash
) {
my
$old
=
$oldhash
->{
$_
};
if
( !
exists
$newhash
->{
$_
} ) {
$changes
{
$_
} = [
$old
,
undef
]
if
defined
$old
;
next
;
}
my
$new
=
$newhash
->{
$_
};
$changes
{
$_
} = [
$old
,
$new
]
unless
!
defined
$old
and !
defined
$new
or
defined
$old
and
defined
$new
and
$old
eq
$new
;
}
foreach
(
keys
%$newhash
) {
my
$new
=
$newhash
->{
$_
};
next
if
exists
$oldhash
->{
$_
};
$changes
{
$_
} = [
undef
,
$new
]
if
defined
$new
;
}
return
keys
%changes
? \
%changes
:
undef
;
}
sub
_do_GET_json
{
my
$self
=
shift
;
my
(
$path
,
@args
) =
@_
;
$self
->{matrix}->_do_GET_json(
"/rooms/$self->{room_id}"
.
$path
,
@args
);
}
sub
_do_PUT_json
{
my
$self
=
shift
;
my
(
$path
,
$content
) =
@_
;
$self
->{matrix}->_do_PUT_json(
"/rooms/$self->{room_id}"
.
$path
,
$content
);
}
sub
_do_POST_json
{
my
$self
=
shift
;
my
(
$path
,
$content
) =
@_
;
$self
->{matrix}->_do_POST_json(
"/rooms/$self->{room_id}"
.
$path
,
$content
);
}
sub
_reset_for_sync
{
my
$self
=
shift
;
undef
$self
->{synced_future};
}
sub
_incoming_sync_invite
{
my
$self
=
shift
;
my
(
$sync
) =
@_
;
warn
"TODO handle incoming sync data in invite state"
;
}
sub
_incoming_sync_join
{
my
$self
=
shift
;
my
(
$sync
) =
@_
;
my
$initial
= not
$self
->await_synced->is_done;
my
$live_state
=
$self
->live_state;
if
(
$sync
->{state} and
$sync
->{state}{events} and @{
$sync
->{state}{events} } ) {
foreach
my
$event
( @{
$sync
->{state}{events} } ) {
$live_state
->handle_event(
$event
);
}
}
foreach
my
$event
( @{
$sync
->{timeline}{events} } ) {
if
(
defined
$event
->{state_key} ) {
my
$old_event
=
$live_state
->get_event(
$event
->{type},
$event
->{state_key} );
$live_state
->handle_event(
$event
);
$self
->_handle_state_event(
$old_event
,
$event
,
$live_state
);
}
else
{
$self
->_handle_event(
forward
=>
$event
);
}
}
foreach
my
$event
( @{
$sync
->{ephemeral}{events} } ) {
$self
->_handle_event(
ephemeral
=>
$event
);
}
if
(
$initial
) {
$self
->await_synced->done;
$self
->maybe_invoke_event(
on_synced_state
=> );
}
}
sub
_incoming_sync_leave
{
my
$self
=
shift
;
my
(
$sync
) =
@_
;
}
sub
await_synced
{
my
$self
=
shift
;
return
$self
->{synced_future} //=
$self
->loop->new_future;
}
sub
live_state
{
my
$self
=
shift
;
return
$self
->{live_state};
}
sub
_handle_state_event
{
my
$self
=
shift
;
my
(
$old_event
,
$new_event
,
$state
) =
@_
;
my
$old_content
=
$old_event
->{content};
my
$new_content
=
$new_event
->{content};
my
%changes
;
$changes
{
$_
}->[0] =
$old_content
->{
$_
}
for
keys
%$old_content
;
$changes
{
$_
}->[1] =
$new_content
->{
$_
}
for
keys
%$new_content
;
$_
->[1] //=
undef
for
values
%changes
;
_delete_null_changes \
%changes
;
my
$member
=
$state
->member(
$new_event
->{sender} );
my
$type
=
$new_event
->{type};
$type
=~ m/^m\.room\.(.*)$/;
my
$method
= $1 ?
"_handle_state_event_"
.
join
(
"_"
,
split
m/\./, $1 ) :
undef
;
if
(
$method
and
my
$code
=
$self
->can(
$method
) ) {
$self
->
$code
(
$member
,
$new_event
,
$state
,
%changes
);
}
else
{
$self
->maybe_invoke_event(
on_state_changed
=>
$member
,
$new_event
,
%changes
);
}
}
sub
_handle_event
{
my
$self
=
shift
;
my
(
$direction
,
$event
) =
@_
;
$event
->{type} =~ m/^(m\.room\.)?(.*)$/ or
return
;
my
$base
= $1 ?
"_handle_roomevent_"
:
"_handle_event_"
;
my
$method
=
$base
.
join
(
"_"
,
split
( m/\./, $2 ),
$direction
);
if
(
my
$code
=
$self
->can(
$method
) ) {
$code
->(
$self
,
$event
);
}
else
{
warn
"TODO: $direction event $event->{type}\n"
;
}
}
sub
_handle_state_backward
{
my
$self
=
shift
;
my
(
$field
,
$event
) =
@_
;
my
$newvalue
=
$event
->{content}{
$field
};
my
$oldvalue
=
$event
->{prev_content}{
$field
};
$self
->maybe_invoke_event(
on_back_state_changed
=>
$self
->{back_members_by_userid}{
$event
->{user_id}},
$event
,
$field
=> [
$newvalue
,
$oldvalue
]
);
}
sub
room_id
{
my
$self
=
shift
;
return
$self
->{room_id};
}
sub
_handle_roomevent_name_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
$self
->_handle_state_backward(
name
=>
$event
);
}
sub
name
{
my
$self
=
shift
;
return
$self
->live_state->name ||
$self
->room_id;
}
sub
set_name
{
my
$self
=
shift
;
my
(
$name
) =
@_
;
$self
->_do_PUT_json(
"/state/m.room.name"
, {
name
=>
$name
} )
->then_done();
}
sub
_handle_state_event_aliases
{
my
$self
=
shift
;
my
(
$member
,
$event
,
$state
,
%changes
) =
@_
;
my
$homeserver
=
$event
->{state_key};
my
@others
=
map
{
$_
->{content}{aliases} }
grep
{
$_
->{state_key} ne
$homeserver
}
values
%{
$state
->get_events(
"m.room.aliases"
) };
$changes
{aliases}[2] = \
@others
;
$self
->maybe_invoke_event(
on_state_changed
=>
$member
,
$event
,
%changes
);
}
sub
_handle_roomevent_aliases_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
my
$homeserver
=
$event
->{state_key};
my
$new
=
$event
->{prev_content}{aliases} // [];
my
$old
=
$event
->{content}{aliases} // [];
$self
->{back_aliases_by_hs}{
$homeserver
} = [
@$new
];
my
@others
=
map
{ @{
$self
->{back_aliases_by_hs}{
$_
} } }
grep
{
$_
ne
$homeserver
}
keys
%{
$self
->{back_aliases_by_hs} };
$self
->maybe_invoke_event(
on_back_state_changed
=>
$self
->{back_members_by_userid}{
$event
->{user_id}},
$event
,
aliases
=> [
$old
,
$new
, \
@others
]
);
}
sub
aliases
{
my
$self
=
shift
;
return
$self
->live_state->aliases;
}
sub
_handle_roomevent_join_rules_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
$self
->_handle_state_backward(
join_rule
=>
$event
);
}
sub
join_rule
{
my
$self
=
shift
;
return
$self
->live_state->join_rule;
}
sub
_handle_roomevent_topic_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
$self
->_handle_state_backward(
topic
=>
$event
);
}
sub
topic
{
my
$self
=
shift
;
return
$self
->live_state->topic;
}
sub
set_topic
{
my
$self
=
shift
;
my
(
$topic
) =
@_
;
$self
->_do_PUT_json(
"/state/m.room.topic"
, {
topic
=>
$topic
} )
->then_done();
}
sub
_handle_generic_level
{
my
$self
=
shift
;
my
(
$phase
,
$level
,
$convert
,
$event
) =
@_
;
foreach
my
$k
(
qw( content prev_content )
) {
next
unless
my
$levels
=
$event
->{
$k
};
$event
->{
$k
} = {
map
{
$convert
->{
$_
} =>
$levels
->{
$_
} }
keys
%$convert
};
}
if
(
$phase
eq
"initial"
) {
my
$levels
=
$event
->{content};
$self
->{levels}{
$_
} =
$levels
->{
$_
}
for
keys
%$levels
;
}
elsif
(
$phase
eq
"forward"
) {
my
$newlevels
=
$event
->{content};
my
$oldlevels
=
$event
->{prev_content};
my
%changes
;
foreach
(
keys
%$newlevels
) {
$self
->{levels}{
$_
} =
$newlevels
->{
$_
};
$changes
{
"level.$_"
} = [
$oldlevels
->{
$_
},
$newlevels
->{
$_
} ]
if
!
defined
$oldlevels
->{
$_
} or
$oldlevels
->{
$_
} !=
$newlevels
->{
$_
};
}
my
$member
=
$self
->member(
$event
->{sender} );
$self
->maybe_invoke_event(
on_state_changed
=>
$member
,
$event
,
%changes
);
}
elsif
(
$phase
eq
"backward"
) {
my
$newlevels
=
$event
->{content};
my
$oldlevels
=
$event
->{prev_content};
my
%changes
;
foreach
(
keys
%$newlevels
) {
$changes
{
"level.$_"
} = [
$newlevels
->{
$_
},
$oldlevels
->{
$_
} ]
if
!
defined
$oldlevels
->{
$_
} or
$oldlevels
->{
$_
} !=
$newlevels
->{
$_
};
}
my
$member
=
$self
->{back_members_by_userid}{
$event
->{user_id}};
$self
->maybe_invoke_event(
on_back_state_changed
=>
$member
,
$event
,
%changes
);
}
}
sub
levels
{
my
$self
=
shift
;
return
%{
$self
->{levels} };
}
sub
change_levels
{
my
$self
=
shift
;
my
%levels
=
@_
;
foreach
(
keys
%levels
) {
delete
$levels
{
$_
}
if
$self
->{levels}{
$_
} ==
$levels
{
$_
};
}
my
%events
;
foreach
(
qw( send_event add_state )
) {
$events
{
"${_}_level"
} = {
level
=>
$levels
{
$_
} }
if
exists
$levels
{
$_
};
}
foreach
(
qw( ban kick redact )
) {
$events
{ops_levels}{
"${_}_level"
} =
$levels
{
$_
}
if
exists
$levels
{
$_
};
}
if
(
$events
{ops_levels} ) {
$events
{ops_levels}{
"${_}_level"
} //=
$self
->{levels}{
$_
}
for
qw( ban kick redact )
;
}
Future->needs_all(
map
{
$self
->_do_PUT_json(
"/state/m.room.$_"
,
$events
{
$_
} ) }
keys
%events
)->then_done();
}
sub
_handle_roomevent_member_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
}
sub
_handle_state_event_member
{
my
$self
=
shift
;
my
(
$member
,
$event
,
$state
,
%changes
) =
@_
;
foreach
my
$idx
( 0, 1 ) {
next
unless
(
$changes
{membership}[
$idx
] //
""
) eq
"leave"
;
undef
$changes
{
$_
}[
$idx
]
for
keys
%changes
;
}
my
$user_id
=
$event
->{state_key};
my
$target_member
=
$state
->member(
$user_id
) or
warn
"ARGH: roomevent_member with unknown user id '$user_id'"
and
return
;
_delete_null_changes \
%changes
;
$self
->maybe_invoke_event(
on_membership
=>
$member
,
$event
,
$target_member
,
%changes
);
}
sub
members
{
my
$self
=
shift
;
return
$self
->live_state->members;
}
sub
member
{
my
$self
=
shift
;
my
(
$user_id
) =
@_
;
return
$self
->live_state->member(
$user_id
);
}
sub
joined_members
{
my
$self
=
shift
;
return
grep
{ (
$_
->membership //
""
) eq
"join"
}
$self
->members;
}
sub
_handle_roomevent_power_levels_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
}
sub
_handle_state_event_power_levels
{
my
$self
=
shift
;
my
(
$member
,
$event
,
$state
,
%changes
) =
@_
;
$_
and
$_
= _pushdown_changes
$_
for
$changes
{users},
$changes
{events};
if
(
my
$users
=
$changes
{users} ) {
my
$default
=
$event
->{content}{user_default};
foreach
my
$user_id
(
keys
%$users
) {
my
$target
=
$state
->member(
$user_id
) or
next
;
my
(
$oldlevel
,
$newlevel
) = @{
$users
->{
$user_id
} };
$oldlevel
//=
$default
;
$newlevel
//=
$default
;
$self
->maybe_invoke_event(
on_membership
=>
$member
,
$event
,
$target
,
level
=> [
$oldlevel
,
$newlevel
]
);
}
}
}
sub
member_level
{
my
$self
=
shift
;
my
(
$user_id
) =
@_
;
return
$self
->live_state->member_level(
$user_id
);
}
sub
change_member_levels
{
my
$self
=
shift
;
my
%user_levels
= %{
$self
->{powerlevels}{users} };
while
(
@_
) {
my
$user_id
=
shift
;
my
$value
=
shift
;
if
(
defined
$value
) {
$user_levels
{
$user_id
} =
$value
;
}
else
{
delete
$user_levels
{
$user_id
};
}
}
$self
->_do_PUT_json(
"/state/m.room.power_levels"
,
{ %{
$self
->{powerlevels} },
users
=> \
%user_levels
}
)->then_done();
}
sub
leave
{
my
$self
=
shift
;
$self
->_do_POST_json(
"/leave"
, {} );
}
sub
invite
{
my
$self
=
shift
;
my
(
$user_id
) =
@_
;
$self
->_do_POST_json(
"/invite"
, {
user_id
=>
$user_id
} )
->then_done();
}
sub
kick
{
my
$self
=
shift
;
my
(
$user_id
,
$reason
) =
@_
;
$self
->_do_POST_json(
"/kick"
, {
user_id
=>
$user_id
,
reason
=>
$reason
} )
->then_done();
}
my
%MSG_REQUIRED_FIELDS
= (
'm.text'
=> [
qw( body )
],
'm.emote'
=> [
qw( body )
],
'm.notice'
=> [
qw( body )
],
'm.image'
=> [
qw( url )
],
'm.audio'
=> [
qw( url )
],
'm.video'
=> [
qw( url )
],
'm.file'
=> [
qw( url )
],
'm.location'
=> [
qw( geo_uri )
],
);
sub
send_message
{
my
$self
=
shift
;
my
%args
= (
@_
== 1 ) ? (
type
=>
"m.text"
,
body
=>
shift
) :
@_
;
my
$type
=
$args
{msgtype} =
delete
$args
{type} or
croak
"Require a 'type' field"
;
$MSG_REQUIRED_FIELDS
{
$type
} or
croak
"Unrecognised message type '$type'"
;
foreach
(@{
$MSG_REQUIRED_FIELDS
{
$type
} } ) {
$args
{
$_
} or croak
"'$type' messages require a '$_' field"
;
}
if
(
defined
(
my
$txn_id
=
$args
{txn_id} ) ) {
$self
->_do_PUT_json(
"/send/m.room.message/$txn_id"
, \
%args
)->then(
sub
{
my
(
$response
) =
@_
;
Future->done(
$response
->{event_id} );
});
}
else
{
$self
->_do_POST_json(
"/send/m.room.message"
, \
%args
)->then(
sub
{
my
(
$response
) =
@_
;
Future->done(
$response
->{event_id} );
});
}
}
sub
paginate_messages
{
my
$self
=
shift
;
my
%args
=
@_
;
my
$limit
=
$args
{limit} // 20;
my
$from
=
$self
->{pagination_token} //
"END"
;
croak
"Cannot paginate_messages any further since we're already at the start"
if
$from
eq
"START"
;
$self
->{back_members_by_userid} //= {
pairmap {
$a
=> Member(
$b
->user,
$b
->displayname,
$b
->membership ) } %{
$self
->{members_by_userid} }
};
$self
->{back_aliases_by_hs} //= {
pairmap {
$a
=> [
@$b
] } %{
$self
->{aliases_by_hs} }
};
my
$f
=
$self
->_do_GET_json(
"/messages"
,
from
=>
$from
,
dir
=>
"b"
,
limit
=>
$limit
,
)->then(
sub
{
my
(
$response
) =
@_
;
foreach
my
$event
( @{
$response
->{chunk} } ) {
next
unless
my
(
$subtype
) = (
$event
->{type} =~ m/^m\.room\.(.*)$/ );
$subtype
=~ s/\./_/g;
if
(
my
$code
=
$self
->can(
"_handle_roomevent_${subtype}_backward"
) ) {
$code
->(
$self
,
$event
);
}
else
{
$self
->{matrix}->
log
(
"TODO: Handle room pagination event $subtype"
);
}
}
$self
->{pagination_token} =
$response
->{end};
Future->done(
$self
);
});
$self
->adopt_future(
$f
);
}
sub
typing_start
{
my
$self
=
shift
;
return
if
$self
->{typing_timer};
my
$user_id
=
$self
->{matrix}->myself->user_id;
my
$f
=
$self
->{typing_timer} = repeat {
$self
->_do_PUT_json(
"/typing/$user_id"
, {
typing
=> 1,
timeout
=> ( TYPING_RESEND_SECONDS + 5 ) * 1000,
})->then(
sub
{
$self
->{matrix}->{make_delay}->( TYPING_RESEND_SECONDS );
});
}
while
=>
sub
{ !
shift
->failure };
$f
->on_fail(
$self
->_capture_weakself(
sub
{
my
$self
=
shift
;
$self
->invoke_error(
@_
);
}));
}
sub
typing_stop
{
my
$self
=
shift
;
return
unless
my
$f
=
$self
->{typing_timer};
$f
->cancel;
undef
$self
->{typing_timer};
my
$user_id
=
$self
->{matrix}->myself->user_id;
$self
->adopt_future(
$self
->_do_PUT_json(
"/typing/$user_id"
, {
typing
=> 0,
})
);
}
sub
send_read_receipt
{
my
$self
=
shift
;
my
%args
=
@_
;
my
$event_id
=
$args
{event_id} or croak
"Require event_id"
;
$self
->_do_POST_json(
"/receipt/m.read/$event_id"
, {} );
}
sub
_handle_roomevent_create_forward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
}
*_handle_roomevent_create_initial
= \
&_handle_roomevent_create_forward
;
sub
_handle_roomevent_create_backward
{
my
$self
=
shift
;
$self
->{pagination_token} =
"START"
;
}
sub
_handle_roomevent_message_forward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
my
$user_id
=
$event
->{sender};
my
$member
=
$self
->member(
$user_id
) or
warn
"TODO: Unknown member '$user_id' for forward message"
and
return
;
$self
->maybe_invoke_event(
on_message
=>
$member
,
$event
->{content},
$event
);
}
sub
_handle_roomevent_message_backward
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
my
$user_id
=
$event
->{user_id};
my
$member
=
$self
->{back_members_by_userid}{
$user_id
} or
warn
"TODO: Unknown member '$user_id' for backward message"
and
return
;
$self
->maybe_invoke_event(
on_back_message
=>
$member
,
$event
->{content},
$event
);
}
sub
_handle_event_m_presence
{
my
$self
=
shift
;
my
(
$user
,
%changes
) =
@_
;
my
$member
=
$self
->member(
$user
->user_id ) or
return
;
$changes
{
$_
} and
$member
->
$_
=
$changes
{
$_
}[1]
for
qw( displayname )
;
$self
->maybe_invoke_event(
on_presence
=>
$member
,
%changes
);
}
sub
_handle_event_m_typing_ephemeral
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
my
$typing
=
$self
->{typing_members};
my
%not_typing
=
%$typing
;
foreach
my
$user_id
( @{
$event
->{content}{user_ids} } ) {
delete
$not_typing
{
$user_id
};
next
if
$typing
->{
$user_id
};
$typing
->{
$user_id
}++;
my
$member
=
$self
->member(
$user_id
) or
next
;
$self
->maybe_invoke_event(
on_typing
=>
$member
, 1 );
}
foreach
my
$user_id
(
keys
%not_typing
) {
my
$member
=
$self
->member(
$user_id
) or
next
;
$self
->maybe_invoke_event(
on_typing
=>
$member
, 0 );
delete
$typing
->{
$user_id
};
}
my
@members
=
map
{
$self
->member(
$_
) }
keys
%$typing
;
$self
->maybe_invoke_event(
on_members_typing
=>
grep
{
defined
}
@members
);
}
sub
_handle_event_m_receipt_ephemeral
{
my
$self
=
shift
;
my
(
$event
) =
@_
;
my
$content
=
$event
->{content};
foreach
my
$event_id
(
keys
%$content
) {
my
$receipt
=
$content
->{
$event_id
};
my
$read_receipt
=
$receipt
->{
"m.read"
} or
next
;
foreach
my
$user_id
(
keys
%$read_receipt
) {
my
$content
=
$read_receipt
->{
$user_id
};
my
$member
=
$self
->member(
$user_id
) or
next
;
$self
->maybe_invoke_event(
on_read_receipt
=>
$member
,
$event_id
,
$content
);
}
}
}
0x55AA;