social_stream 0.21.4 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +11 -5
- data/base/app/assets/stylesheets/cheesecake.css.scss +1 -0
- data/base/app/views/cheesecake/_cheesecake.html.erb +3 -0
- data/base/app/views/cheesecake/_index.html.erb +88 -41
- data/base/app/views/cheesecake/_sector_form.html.erb +12 -12
- data/base/lib/social_stream/base/version.rb +1 -1
- data/base/lib/social_stream/test_helpers/controllers.rb +19 -2
- data/base/lib/tasks/db/populate.rake +190 -165
- data/base/social_stream-base.gemspec +1 -1
- data/base/spec/controllers/posts_controller_spec.rb +19 -1
- data/lib/social_stream/version.rb +1 -1
- data/presence/app/assets/images/status/chat.png +0 -0
- data/presence/app/assets/javascripts/chat_interface_manager.js.erb +42 -45
- data/presence/app/assets/javascripts/chat_parser.js +5 -5
- data/presence/app/assets/javascripts/chat_persistence.js +25 -26
- data/presence/app/assets/javascripts/chat_utilities.js +15 -11
- data/presence/app/assets/javascripts/chat_window_manager.js +129 -26
- data/presence/app/assets/javascripts/jquery.ui.chatbox.sstreampresence.js +22 -3
- data/presence/app/assets/javascripts/social_stream-presence.js +1 -0
- data/presence/app/assets/javascripts/videochat.js.erb +459 -0
- data/presence/app/assets/javascripts/xmpp_client_management.js.erb +303 -65
- data/presence/app/assets/stylesheets/chat.css.scss +42 -1
- data/presence/app/controllers/xmpp_controller.rb +20 -3
- data/presence/app/views/chat/_index.html.erb +7 -2
- data/presence/app/views/xmpp/getOpenTokSessionIDAndToken.xml.builder +6 -0
- data/presence/config/locales/en.yml +22 -1
- data/presence/config/locales/es.yml +23 -2
- data/presence/config/routes.rb +2 -0
- data/presence/ejabberd/conf/ssconfig_example.cfg +4 -3
- data/presence/ejabberd/ejabberd_files.zip +0 -0
- data/presence/ejabberd/ejabberd_scripts/authentication_script +22 -12
- data/presence/ejabberd/ejabberd_scripts/development_scripts/show_config.sh +9 -10
- data/presence/ejabberd/ejabberd_scripts/emanagement +275 -178
- data/presence/ejabberd/ejabberd_scripts/manageWebDomains +164 -0
- data/presence/ejabberd/ejabberd_scripts/rest_api_client_script +75 -32
- data/presence/ejabberd/ejabberd_scripts/synchronize_presence_script +81 -34
- data/presence/ejabberd/mod_sspresence/mod_sspresence.beam +0 -0
- data/presence/ejabberd/mod_sspresence/mod_sspresence.erl +27 -23
- data/presence/lib/OpenTok/Exceptions.rb +11 -0
- data/presence/lib/OpenTok/OpenTokSDK.rb +184 -0
- data/presence/lib/OpenTok/Session.rb +27 -0
- data/presence/lib/generators/social_stream/presence/templates/initializer.rb +8 -2
- data/presence/lib/open_tok.rb +31 -0
- data/presence/lib/opentok.rb +29 -0
- data/presence/lib/social_stream-presence.rb +4 -0
- data/presence/lib/social_stream/presence/models/buddy_manager.rb +1 -1
- data/presence/lib/social_stream/presence/version.rb +1 -1
- data/presence/lib/social_stream/presence/xmpp_server_order.rb +96 -76
- data/presence/lib/social_stream/presence/xmpp_ssclient.rb +54 -0
- data/presence/lib/tasks/presence/multidomain.rake +45 -0
- data/presence/lib/tasks/presence/synchronize.rake +18 -4
- data/presence/vendor/assets/javascripts/TB.min.js +4329 -0
- data/social_stream.gemspec +2 -2
- metadata +40 -30
- data/presence/ejabberd/ejabberd_scripts/reset_connection_script +0 -300
- data/presence/ejabberd/ejabberd_scripts/set_script_header.sh +0 -112
Binary file
|
@@ -1,9 +1,10 @@
|
|
1
1
|
%%%-------------------------------------------------------------------
|
2
2
|
%%% File : mod_sspresence.erl
|
3
|
-
%%% Author : Aldo
|
4
|
-
%%% Contact:
|
3
|
+
%%% Author : Aldo Gordillo
|
4
|
+
%%% Contact : < social-stream@dit.upm.es >
|
5
5
|
%%% Purpose : Process events and hooks for Social Stream Presence: http://social-stream.dit.upm.es/
|
6
6
|
%%% Created : 1 Oct 2011
|
7
|
+
%%% Version : 2.0
|
7
8
|
%%%
|
8
9
|
%%%
|
9
10
|
%%% http://social-stream.dit.upm.es/
|
@@ -50,42 +51,46 @@ stop(Host) ->
|
|
50
51
|
|
51
52
|
|
52
53
|
on_register_connection(_SID, _JID, _Info) ->
|
53
|
-
{_A,User,
|
54
|
-
|
54
|
+
{_A,User,Domain,_C,_D,_E,_F} = _JID,
|
55
|
+
UserJid = string:join([User, Domain ], "@"),
|
56
|
+
?INFO_MSG("mod_sspresence: on_register_connection (~p)", [UserJid]),
|
55
57
|
Rest_api_script_path = string:concat(getOptionValue("scripts_path="), "/rest_api_client_script "),
|
56
|
-
os:cmd(string:join([Rest_api_script_path, "setConnection",
|
58
|
+
os:cmd(string:join([Rest_api_script_path, "setConnection", UserJid ], " ")),
|
57
59
|
ok.
|
58
60
|
|
59
61
|
on_remove_connection(_SID, _JID, _SessionInfo) ->
|
60
|
-
{_A,User,
|
61
|
-
|
62
|
-
|
62
|
+
{_A,User,Domain,_C,_D,_E,_F} = _JID,
|
63
|
+
UserJid = string:join([User, Domain ], "@"),
|
64
|
+
?INFO_MSG("mod_sspresence: on_remove_connection (~p)", [UserJid]),
|
65
|
+
Connected = isConnected(UserJid),
|
63
66
|
case Connected of
|
64
67
|
true -> ok;
|
65
68
|
_ -> Rest_api_script_path = string:concat(getOptionValue("scripts_path="), "/rest_api_client_script "),
|
66
|
-
os:cmd(string:join([Rest_api_script_path, "unsetConnection",
|
69
|
+
os:cmd(string:join([Rest_api_script_path, "unsetConnection", UserJid ], " "))
|
67
70
|
end,
|
68
71
|
ok.
|
69
72
|
|
70
|
-
on_presence(User,
|
71
|
-
|
73
|
+
on_presence(User, Server, _Resource, Packet) ->
|
74
|
+
UserJid = string:join([User, Server ], "@"),
|
75
|
+
?INFO_MSG("mod_sspresence: on_presence (~p)", [UserJid]),
|
72
76
|
{_xmlelement, Type, _Attr, Subel} = Packet,
|
73
77
|
|
74
78
|
case Type of
|
75
79
|
"presence" -> Status = getStatusFromSubel(Subel),
|
76
80
|
Rest_api_script_path = string:concat(getOptionValue("scripts_path="), "/rest_api_client_script "),
|
77
|
-
?INFO_MSG("mod_sspresence: set_presence_script call with
|
78
|
-
os:cmd(string:join([Rest_api_script_path, "setPresence",
|
81
|
+
?INFO_MSG("mod_sspresence: set_presence_script call with userJid (~p) and status (~p)", [UserJid,Status]),
|
82
|
+
os:cmd(string:join([Rest_api_script_path, "setPresence", UserJid , Status], " "));
|
79
83
|
_ -> ok
|
80
84
|
end,
|
81
85
|
ok.
|
82
86
|
|
83
|
-
on_unset_presence(User,
|
84
|
-
|
87
|
+
on_unset_presence(User, Server, _Resource, _Status) ->
|
88
|
+
UserJid = string:join([User, Server ], "@"),
|
89
|
+
?INFO_MSG("mod_sspresence: on_unset_presence (~p)", [UserJid]),
|
85
90
|
_Rest_api_script_path = string:concat(getOptionValue("scripts_path="), "/rest_api_client_script "),
|
86
91
|
%% Wait for on_remove_connection
|
87
|
-
%% ?INFO_MSG("mod_sspresence: unset_presence_script call with
|
88
|
-
%%os:cmd(string:join([_Rest_api_script_path, "unsetPresence",
|
92
|
+
%% ?INFO_MSG("mod_sspresence: unset_presence_script call with userJid (~p)", [UserJid]),
|
93
|
+
%%os:cmd(string:join([_Rest_api_script_path, "unsetPresence", UserJid], " ")),
|
89
94
|
ok.
|
90
95
|
|
91
96
|
on_packet_send(_From, _To, {xmlelement, Type, _Attr, _Subel} = _Packet) ->
|
@@ -158,9 +163,9 @@ parser(In,Option) ->
|
|
158
163
|
|
159
164
|
|
160
165
|
%%Check if a user is connected (any active session with Ejabberd server)
|
161
|
-
isConnected(
|
166
|
+
isConnected(UserJid) ->
|
162
167
|
|
163
|
-
Command = string:concat("ejabberdctl connected-users | grep ",
|
168
|
+
Command = string:concat("ejabberdctl connected-users | grep ", UserJid),
|
164
169
|
Output = os:cmd(Command),
|
165
170
|
|
166
171
|
case Output of
|
@@ -169,11 +174,10 @@ case Output of
|
|
169
174
|
|
170
175
|
catch lists:foreach(
|
171
176
|
fun(S) ->
|
172
|
-
[
|
173
|
-
%User.slug connected = Slug
|
177
|
+
[Jid|_R] = string:tokens(S, "/"),
|
174
178
|
|
175
|
-
case
|
176
|
-
|
179
|
+
case Jid of
|
180
|
+
UserJid -> throw(true);
|
177
181
|
_ -> false
|
178
182
|
end
|
179
183
|
|
@@ -0,0 +1,184 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
|
3
|
+
|
4
|
+
=begin
|
5
|
+
OpenTok Ruby Library
|
6
|
+
http://www.tokbox.com/
|
7
|
+
|
8
|
+
Copyright 2010, TokBox, Inc.
|
9
|
+
|
10
|
+
=end
|
11
|
+
|
12
|
+
require 'cgi'
|
13
|
+
require 'openssl'
|
14
|
+
require 'base64'
|
15
|
+
require 'uri'
|
16
|
+
require 'net/https'
|
17
|
+
require 'rexml/document'
|
18
|
+
|
19
|
+
DIGEST = OpenSSL::Digest::Digest.new('sha1')
|
20
|
+
|
21
|
+
class Hash
|
22
|
+
def urlencode
|
23
|
+
to_a.map do |name_value|
|
24
|
+
if name_value[1].is_a? Array
|
25
|
+
name_value[0] = CGI.escape name_value[0].to_s
|
26
|
+
name_value[1].map { |e| CGI.escape e.to_s }
|
27
|
+
name_value[1] = name_value[1].join "&" + name_value[0] + "="
|
28
|
+
name_value.join '='
|
29
|
+
else
|
30
|
+
name_value.map { |e| CGI.escape e.to_s }.join '='
|
31
|
+
end
|
32
|
+
end.join '&'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module OpenTok
|
37
|
+
|
38
|
+
class SessionPropertyConstants
|
39
|
+
ECHOSUPPRESSION_ENABLED = "echoSuppression.enabled"; #Boolean
|
40
|
+
MULTIPLEXER_NUMOUTPUTSTREAMS = "multiplexer.numOutputStreams"; #Integer
|
41
|
+
MULTIPLEXER_SWITCHTYPE = "multiplexer.switchType"; #Integer
|
42
|
+
MULTIPLEXER_SWITCHTIMEOUT = "multiplexer.switchTimeout"; #Integer
|
43
|
+
P2P_PREFERENCE = "p2p.preference"; #String
|
44
|
+
end
|
45
|
+
|
46
|
+
class RoleConstants
|
47
|
+
SUBSCRIBER = "subscriber" #Can only subscribe
|
48
|
+
PUBLISHER = "publisher" #Can publish, subscribe, and signal
|
49
|
+
MODERATOR = "moderator" #Can do the above along with forceDisconnect and forceUnpublish
|
50
|
+
end
|
51
|
+
|
52
|
+
class Net::HTTP
|
53
|
+
alias_method :old_initialize, :initialize
|
54
|
+
def initialize(*args)
|
55
|
+
old_initialize(*args)
|
56
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
57
|
+
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.included(base)
|
62
|
+
# Initialize module.
|
63
|
+
end
|
64
|
+
|
65
|
+
class OpenTokSession
|
66
|
+
attr_accessor :session_id
|
67
|
+
|
68
|
+
def initialize(session_id)
|
69
|
+
@session_id = session_id
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
session_id
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class OpenTokSDK
|
78
|
+
attr_writer :api_url
|
79
|
+
@@TOKEN_SENTINEL = "T1=="
|
80
|
+
@@SDK_VERSION = "tbruby-%s" % [ VERSION ]
|
81
|
+
|
82
|
+
# @@API_URL = API_URL
|
83
|
+
|
84
|
+
def initialize(partner_id, partner_secret)
|
85
|
+
@api_url = API_URL
|
86
|
+
@partner_id = partner_id
|
87
|
+
@partner_secret = partner_secret.strip
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_token(opts = {})
|
91
|
+
{:session_id=>nil, :create_time=>nil, :expire_time=>nil, :role=>nil, :connection_data=>nil}.merge!(opts)
|
92
|
+
|
93
|
+
create_time = opts[:create_time].nil? ? Time.now : opts[:create_time]
|
94
|
+
session_id = opts[:session_id].nil? ? '' : opts[:session_id]
|
95
|
+
role = opts[:role].nil? ? RoleConstants::PUBLISHER : opts[:role]
|
96
|
+
|
97
|
+
if role != RoleConstants::SUBSCRIBER and \
|
98
|
+
role != RoleConstants::PUBLISHER and \
|
99
|
+
role != RoleConstants::MODERATOR
|
100
|
+
raise OpenTokException.new "'#{role}' is not a recognized role"
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
data_params = {
|
105
|
+
:role => role,
|
106
|
+
:session_id => session_id,
|
107
|
+
:create_time => create_time.to_i,
|
108
|
+
:nonce => rand
|
109
|
+
}
|
110
|
+
|
111
|
+
if not opts[:expire_time].nil?
|
112
|
+
raise OpenTokException.new 'Expire time must be a number' if not opts[:expire_time].is_a?(Numeric)
|
113
|
+
raise OpenTokException.new 'Expire time must be in the future' if opts[:expire_time] < Time.now.to_i
|
114
|
+
raise OpenTokException.new 'Expire time must be in the next 7 days' if opts[:expire_time] > (Time.now.to_i + 604800)
|
115
|
+
data_params[:expire_time] = opts[:expire_time].to_i
|
116
|
+
end
|
117
|
+
|
118
|
+
if not opts[:connection_data].nil?
|
119
|
+
raise OpenTokException.new 'Connection data must be less than 1000 characters' if opts[:connection_data].length > 1000
|
120
|
+
data_params[:connection_data] = opts[:connection_data]
|
121
|
+
end
|
122
|
+
|
123
|
+
data_string = data_params.urlencode
|
124
|
+
|
125
|
+
sig = sign_string(data_string, @partner_secret)
|
126
|
+
meta_string = {
|
127
|
+
:partner_id => @partner_id,
|
128
|
+
:sdk_version => @@SDK_VERSION,
|
129
|
+
:sig => sig
|
130
|
+
}.urlencode
|
131
|
+
|
132
|
+
@@TOKEN_SENTINEL + Base64.encode64(meta_string + ":" + data_string).gsub("\n","")
|
133
|
+
end
|
134
|
+
|
135
|
+
def create_session(location='', opts={})
|
136
|
+
opts.merge!({:partner_id => @partner_id, :location=>location})
|
137
|
+
doc = do_request("/session/create", opts)
|
138
|
+
if not doc.get_elements('Errors').empty?
|
139
|
+
raise OpenTokException.new doc.get_elements('Errors')[0].get_elements('error')[0].children.to_s
|
140
|
+
end
|
141
|
+
OpenTokSession.new(doc.root.get_elements('Session')[0].get_elements('session_id')[0].children[0].to_s)
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
def sign_string(data, secret)
|
147
|
+
OpenSSL::HMAC.hexdigest(DIGEST, secret, data)
|
148
|
+
end
|
149
|
+
|
150
|
+
def do_request(api_url, params, token=nil)
|
151
|
+
|
152
|
+
url = URI.parse(@api_url + api_url)
|
153
|
+
if not params.empty?
|
154
|
+
req = Net::HTTP::Post.new(url.path)
|
155
|
+
req.set_form_data(params)
|
156
|
+
else
|
157
|
+
req = Net::HTTP::Get.new(url.path)
|
158
|
+
end
|
159
|
+
|
160
|
+
if not token.nil?
|
161
|
+
req.add_field 'X-TB-TOKEN-AUTH', token
|
162
|
+
else
|
163
|
+
req.add_field 'X-TB-PARTNER-AUTH', "#{@partner_id}:#{@partner_secret}"
|
164
|
+
end
|
165
|
+
http = Net::HTTP.new(url.host, url.port)
|
166
|
+
http.use_ssl = true if @api_url.start_with?("https")
|
167
|
+
res = http.start {|http| http.request(req)}
|
168
|
+
case res
|
169
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
170
|
+
# OK
|
171
|
+
doc = REXML::Document.new(res.read_body)
|
172
|
+
return doc
|
173
|
+
else
|
174
|
+
res.error!
|
175
|
+
end
|
176
|
+
rescue Net::HTTPExceptions
|
177
|
+
raise
|
178
|
+
raise OpenTokException.new 'Unable to create fufill request: ' + $!
|
179
|
+
rescue NoMethodError
|
180
|
+
raise
|
181
|
+
raise OpenTokException.new 'Unable to create a fufill request at this time: ' + $1
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
=begin
|
3
|
+
OpenTok Ruby Library v0.90.0
|
4
|
+
http://www.tokbox.com/
|
5
|
+
|
6
|
+
Copyright 2010, TokBox, Inc.
|
7
|
+
|
8
|
+
Date: November 05 14:50:00 2010
|
9
|
+
=end
|
10
|
+
|
11
|
+
module OpenTok
|
12
|
+
|
13
|
+
class Session
|
14
|
+
|
15
|
+
attr_reader :sessionId
|
16
|
+
|
17
|
+
def initialize(sessionId)
|
18
|
+
@sessionId = sessionId
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
sessionId
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
SocialStream::Presence.setup do |config|
|
2
|
-
#Configures
|
2
|
+
#Configures Web Server Domain served by XMPP Server
|
3
3
|
config.domain = "localhost"
|
4
4
|
#Configures Bosh Service Path
|
5
5
|
#config.bosh_service = "http://xmpp-proxy/http-bind"
|
@@ -29,6 +29,12 @@ SocialStream::Presence.setup do |config|
|
|
29
29
|
#Username of the the Social Stream Admin sid
|
30
30
|
config.social_stream_presence_username = "social_stream-presence"
|
31
31
|
#Configures Social Stream Rails App Password
|
32
|
-
config.password = <%= SecureRandom.hex(32).inspect %>
|
32
|
+
config.password = <%= SecureRandom.hex(32).inspect %>
|
33
|
+
|
34
|
+
#OpenTok settings (Only for videochat)
|
35
|
+
#Replace with your OpenTok API key.
|
36
|
+
#config.opentok_api_key = ""
|
37
|
+
#Replace with your OpenTok API secret.
|
38
|
+
#config.opentok_api_secret = ""
|
33
39
|
|
34
40
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
=begin
|
5
|
+
OpenTok Ruby Library
|
6
|
+
http://www.tokbox.com/
|
7
|
+
|
8
|
+
Copyright 2010, TokBox, Inc.
|
9
|
+
|
10
|
+
Last modified: 2011-10-12
|
11
|
+
=end
|
12
|
+
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'net/http'
|
16
|
+
require 'uri'
|
17
|
+
require 'digest/md5'
|
18
|
+
require 'cgi'
|
19
|
+
#require 'pp' # just for debugging purposes
|
20
|
+
|
21
|
+
Net::HTTP.version_1_2 # to make sure version 1.2 is used
|
22
|
+
|
23
|
+
module OpenTok
|
24
|
+
VERSION = "tbrb-v0.91.2011-10-12"
|
25
|
+
#API_URL = "https://staging.tokbox.com/hl"
|
26
|
+
#Uncomment this line when you launch your app
|
27
|
+
API_URL = "https://api.opentok.com/hl";
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'OpenTok/Exceptions'
|
31
|
+
require 'OpenTok/OpenTokSDK'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
=begin
|
5
|
+
OpenTok Ruby Library
|
6
|
+
http://www.tokbox.com/
|
7
|
+
|
8
|
+
Copyright 2010, TokBox, Inc.
|
9
|
+
|
10
|
+
Last modified: 2011-02-17
|
11
|
+
=end
|
12
|
+
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'net/http'
|
16
|
+
require 'uri'
|
17
|
+
require 'digest/md5'
|
18
|
+
require 'cgi'
|
19
|
+
#require 'pp' # just for debugging purposes
|
20
|
+
|
21
|
+
Net::HTTP.version_1_2 # to make sure version 1.2 is used
|
22
|
+
|
23
|
+
module OpenTok
|
24
|
+
VERSION = "tbrb-v0.91.2011-02-17"
|
25
|
+
API_URL = "https://api.opentok.com/hl";
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'OpenTok/Exceptions'
|
29
|
+
require 'OpenTok/OpenTokSDK'
|
@@ -33,12 +33,16 @@ module SocialStream
|
|
33
33
|
|
34
34
|
mattr_accessor :social_stream_presence_username
|
35
35
|
mattr_accessor :password
|
36
|
+
|
37
|
+
mattr_accessor :opentok_api_key
|
38
|
+
mattr_accessor :opentok_api_secret
|
36
39
|
|
37
40
|
@@auth_method = "cookie"
|
38
41
|
@@remote_xmpp_server = false
|
39
42
|
@@secure_rest_api = false
|
40
43
|
@@enable = false
|
41
44
|
@@social_stream_presence_username = "social_stream_presence"
|
45
|
+
@@opentok_api_key = "default"
|
42
46
|
|
43
47
|
class << self
|
44
48
|
def setup
|