plezi 0.7.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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/CHANGELOG.md +450 -0
- data/Gemfile +4 -0
- data/KNOWN_ISSUES.md +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +341 -0
- data/Rakefile +2 -0
- data/TODO.md +19 -0
- data/bin/plezi +301 -0
- data/lib/plezi.rb +125 -0
- data/lib/plezi/base/cache.rb +77 -0
- data/lib/plezi/base/connections.rb +33 -0
- data/lib/plezi/base/dsl.rb +177 -0
- data/lib/plezi/base/engine.rb +85 -0
- data/lib/plezi/base/events.rb +84 -0
- data/lib/plezi/base/io_reactor.rb +41 -0
- data/lib/plezi/base/logging.rb +62 -0
- data/lib/plezi/base/rack_app.rb +89 -0
- data/lib/plezi/base/services.rb +57 -0
- data/lib/plezi/base/timers.rb +71 -0
- data/lib/plezi/handlers/controller_magic.rb +383 -0
- data/lib/plezi/handlers/http_echo.rb +27 -0
- data/lib/plezi/handlers/http_host.rb +215 -0
- data/lib/plezi/handlers/http_router.rb +69 -0
- data/lib/plezi/handlers/magic_helpers.rb +43 -0
- data/lib/plezi/handlers/route.rb +272 -0
- data/lib/plezi/handlers/stubs.rb +143 -0
- data/lib/plezi/server/README.md +33 -0
- data/lib/plezi/server/helpers/http.rb +169 -0
- data/lib/plezi/server/helpers/mime_types.rb +999 -0
- data/lib/plezi/server/protocols/http_protocol.rb +318 -0
- data/lib/plezi/server/protocols/http_request.rb +133 -0
- data/lib/plezi/server/protocols/http_response.rb +294 -0
- data/lib/plezi/server/protocols/websocket.rb +208 -0
- data/lib/plezi/server/protocols/ws_response.rb +92 -0
- data/lib/plezi/server/services/basic_service.rb +224 -0
- data/lib/plezi/server/services/no_service.rb +196 -0
- data/lib/plezi/server/services/ssl_service.rb +193 -0
- data/lib/plezi/version.rb +3 -0
- data/plezi.gemspec +26 -0
- data/resources/404.erb +68 -0
- data/resources/404.haml +64 -0
- data/resources/404.html +67 -0
- data/resources/404.slim +63 -0
- data/resources/500.erb +68 -0
- data/resources/500.haml +63 -0
- data/resources/500.html +67 -0
- data/resources/500.slim +63 -0
- data/resources/Gemfile +85 -0
- data/resources/anorexic_gray.png +0 -0
- data/resources/anorexic_websockets.html +47 -0
- data/resources/code.rb +8 -0
- data/resources/config.ru +39 -0
- data/resources/controller.rb +139 -0
- data/resources/db_ac_config.rb +58 -0
- data/resources/db_dm_config.rb +51 -0
- data/resources/db_sequel_config.rb +42 -0
- data/resources/en.yml +204 -0
- data/resources/environment.rb +41 -0
- data/resources/haml_config.rb +6 -0
- data/resources/i18n_config.rb +14 -0
- data/resources/rakefile.rb +22 -0
- data/resources/redis_config.rb +35 -0
- data/resources/routes.rb +26 -0
- data/resources/welcome_page.html +72 -0
- data/websocket chatroom.md +639 -0
- metadata +141 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
#########################################
|
2
|
+
# this is your SampleController
|
3
|
+
#
|
4
|
+
# feed it and give it plenty of children, borothers and sisters.
|
5
|
+
#
|
6
|
+
#########################################
|
7
|
+
#
|
8
|
+
# this is a skelaton for a RESTful and WebSocket controller implementation.
|
9
|
+
#
|
10
|
+
# it can also be used for non RESTful requests by utilizing only the
|
11
|
+
# index method or other, non-RESTful, named methods.
|
12
|
+
#
|
13
|
+
# if a method returns false, an 404 not found is assumed. and routes continue to search.
|
14
|
+
#
|
15
|
+
# otherwise, the method's returned value is added to the response body (if it's a String).
|
16
|
+
#
|
17
|
+
# methods should return true (if the response was set/sent) or the body's string as their last value.
|
18
|
+
#
|
19
|
+
# no inheritance is required (the Plezi framework inherits your code, not the other way around).
|
20
|
+
#
|
21
|
+
# here are some of the available controller properties and methods:
|
22
|
+
#
|
23
|
+
# attr_accessor :params, :request, :response, :cookies
|
24
|
+
#
|
25
|
+
# params:: short-cut for request.params
|
26
|
+
# request:: the request object
|
27
|
+
# response:: the response object.
|
28
|
+
# cookies:: a magic cookie-jar (sets and gets cookies).
|
29
|
+
# flash:: a hash used for one-time cookies (get and set). any added values will be available for the same client for this and the next connection, after which they are removed.
|
30
|
+
#
|
31
|
+
# redirect_to:: easily set up redirection.
|
32
|
+
# send_data:: easily send data, setting the content-type and content-length headers.
|
33
|
+
# render:: easily render Slim, Haml and IRB files into String text.
|
34
|
+
#
|
35
|
+
class SampleController
|
36
|
+
|
37
|
+
# it is possible to includes all the Plezi::FeedHaml helper methods...
|
38
|
+
# ... this breakes the strict MVC architecture by running the HAML view
|
39
|
+
# as part of the controller class. It's good for hacking, but bad practice.
|
40
|
+
# include Plezi::FeedHaml if defined? Plezi::FeedHaml
|
41
|
+
|
42
|
+
# called before any action except WebSockets actions
|
43
|
+
def before
|
44
|
+
# some actions before the request is parsed
|
45
|
+
# you can remove this, ofcourse. the router doesn't require the method to exist.
|
46
|
+
|
47
|
+
## uncomment the following line for a very loose reloading - use for debug mode only.
|
48
|
+
# load Root.join("environment.rb").to_s unless ENV['RACK_ENV'] == 'production'
|
49
|
+
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# called after any action except WebSockets actions
|
54
|
+
def after
|
55
|
+
# some actions after the request is parsed
|
56
|
+
# you can remove this, ofcourse. the router doesn't require the method to exist.
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
# called when request is GET and there's no "id" in quary
|
61
|
+
def index
|
62
|
+
# while using sym redirection (unlike string redirection),
|
63
|
+
# Plezi will attempt to auto-format a valid URL.
|
64
|
+
redirect_to "assets_welcome.html".to_sym
|
65
|
+
end
|
66
|
+
|
67
|
+
# called when the params[:id] == fail. this a demonstration for custom routing.
|
68
|
+
def fail
|
69
|
+
# throw up this code and feed plezi your own lines :)
|
70
|
+
raise "Plezi raising hell!"
|
71
|
+
end
|
72
|
+
|
73
|
+
# called when request is GET and quary defines "id"
|
74
|
+
def show
|
75
|
+
"I'd love to show you object with id: #{params[:id]}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# called when the request is GET and the params[:id] == "new"
|
79
|
+
def new
|
80
|
+
"let's make something new."
|
81
|
+
end
|
82
|
+
|
83
|
+
# called when request is POST or PUT and there's no "id" in quary
|
84
|
+
def save
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
# called when request is POST or PUT and quary defines "id"
|
89
|
+
def update
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
# called when request is DELETE and quary defines "id"
|
94
|
+
def delete
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
# called before the protocol is swithed from HTTP to WebSockets.
|
99
|
+
#
|
100
|
+
# this allows setting headers, cookies and other data (such as authentication)
|
101
|
+
# prior to opening a WebSocket.
|
102
|
+
#
|
103
|
+
# if the method returns false, the connection will be refused and the remaining routes will be attempted.
|
104
|
+
def pre_connect
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
# called immediately after a WebSocket connection has been established.
|
109
|
+
# here we simply close the connection.
|
110
|
+
def on_connect
|
111
|
+
response.close
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
# called when new data is recieved
|
116
|
+
#
|
117
|
+
# data is a string that contains binary or UTF8 (message dependent) data.
|
118
|
+
#
|
119
|
+
# the demo content simply broadcasts the message.
|
120
|
+
def on_message data
|
121
|
+
# broadcast sends an asynchronous message to all sibling instances, but not to self.
|
122
|
+
broadcast :_print_out, data
|
123
|
+
end
|
124
|
+
|
125
|
+
# called when a disconnect packet has been recieved or the connection has been cut
|
126
|
+
# (ISN'T called after a disconnect message has been sent).
|
127
|
+
def on_disconnect
|
128
|
+
end
|
129
|
+
|
130
|
+
# a demo event method that recieves a broadcast from instance siblings.
|
131
|
+
#
|
132
|
+
# methods that are protected and methods that start with an underscore are hidden from the router
|
133
|
+
# BUT, broadcasted methods must be public (or the broadcast will quietly fail)... so we have to use
|
134
|
+
# the _underscore for this method.
|
135
|
+
def _print_out data
|
136
|
+
response << "Someone said #{data}"
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#################
|
2
|
+
## SETTINGS FOR DATABASES
|
3
|
+
#
|
4
|
+
# this file contains common settings and rake tasks for ORM gems.
|
5
|
+
#
|
6
|
+
# please review / edit the settings you need.
|
7
|
+
#
|
8
|
+
############
|
9
|
+
## ActiveRecord without Rails
|
10
|
+
# more info @:
|
11
|
+
# https://www.youtube.com/watch?v=o0SzhgYntK8
|
12
|
+
# demo code here:
|
13
|
+
# https://github.com/stungeye/ActiveRecord-without-Rails
|
14
|
+
if defined? ActiveRecord
|
15
|
+
|
16
|
+
if defined? SQLite3
|
17
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => Root.join('db','db.sqlite3').to_s, encoding: 'unicode'
|
18
|
+
elsif defined? PG
|
19
|
+
ActiveRecord::Base.establish_connection :adapter => 'postgresql', encoding: 'unicode'
|
20
|
+
else
|
21
|
+
Plezi.logger.warning "ActiveRecord database adapter not auto-set. Update the db_ac_config.rb file"
|
22
|
+
end
|
23
|
+
|
24
|
+
# if debugging purposes, uncomment this line to see the ActiveRecord's generated SQL:
|
25
|
+
# ActiveRecord::Base.logger = Plezi.logger
|
26
|
+
|
27
|
+
# Uncomment this line to make the logger output look nicer in Windows.
|
28
|
+
# ActiveSupport::LogSubscriber.colorize_logging = false
|
29
|
+
|
30
|
+
if defined? Rake
|
31
|
+
##########
|
32
|
+
# start rake segment
|
33
|
+
|
34
|
+
namespace :db do
|
35
|
+
|
36
|
+
desc "Migrate the database so that it is fully updated, using db/migrate."
|
37
|
+
task :migrate do
|
38
|
+
ActiveRecord::Base.logger = Plezi.logger
|
39
|
+
ActiveRecord::Migrator.migrate(Root.join('db', 'migrate').to_s, ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "Seed the database using the db/seeds.rb file"
|
43
|
+
task :seed do
|
44
|
+
if ::File.exists? Root.join('db','seeds.rb').to_s
|
45
|
+
load Root.join('db','seeds.rb').to_s
|
46
|
+
else
|
47
|
+
puts "the seeds file doesn't exists. please create a seeds.db file and place it in the db folder for the app."
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
# end rake segment
|
54
|
+
##########
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#################
|
2
|
+
## SETTINGS FOR DATABASES
|
3
|
+
#
|
4
|
+
# this file contains common settings and rake tasks for ORM gems.
|
5
|
+
#
|
6
|
+
# please review / edit the settings you need.
|
7
|
+
#
|
8
|
+
############
|
9
|
+
## DataMapper
|
10
|
+
# more info @:
|
11
|
+
# http://datamapper.org
|
12
|
+
if defined? DataMapper
|
13
|
+
# If you want the logs...
|
14
|
+
DataMapper::Logger.new(Plezi.logger, :debug)
|
15
|
+
|
16
|
+
if defined? SQLite3
|
17
|
+
# An in-memory Sqlite3 connection:
|
18
|
+
DataMapper.setup(:default, 'sqlite::memory:')
|
19
|
+
|
20
|
+
# A Sqlite3 connection to a persistent database
|
21
|
+
DataMapper.setup(:default, "sqlite:///#{Root.join('db', 'db.sqlite3').to_s}")
|
22
|
+
elsif defined? PG
|
23
|
+
# A Postgres connection:
|
24
|
+
if ENV['DYNO']
|
25
|
+
DataMapper.setup(:default, ENV['HEROKU_POSTGRESQL_RED_URL'])
|
26
|
+
else
|
27
|
+
Root.to_s.split(/\/\\/).last
|
28
|
+
DataMapper.setup(:default, "postgres://localhost/#{Root.to_s.split(/[\/\\]/).last}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
if defined? Rake
|
32
|
+
##########
|
33
|
+
# start rake segment
|
34
|
+
|
35
|
+
namespace :db do
|
36
|
+
take :rebuild do
|
37
|
+
DataMapper.finalize.auto_migrate!
|
38
|
+
end
|
39
|
+
task :migrate do
|
40
|
+
DataMapper.finalize.auto_upgrade!
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
# end rake segment
|
46
|
+
##########
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#################
|
2
|
+
## SETTINGS FOR DATABASES
|
3
|
+
#
|
4
|
+
# this file contains common settings and rake tasks for ORM gems.
|
5
|
+
#
|
6
|
+
# please review / edit the settings you need.
|
7
|
+
#
|
8
|
+
############
|
9
|
+
## Sequel
|
10
|
+
# more info @:
|
11
|
+
# http://sequel.jeremyevans.net
|
12
|
+
if defined? Sequel
|
13
|
+
|
14
|
+
if defined? SQLite3
|
15
|
+
# An in-memory Sqlite3 connection:
|
16
|
+
# DB = Sequel.sqlite
|
17
|
+
|
18
|
+
# A Sqlite3 connection to a persistent database
|
19
|
+
DB = Sequel.sqlite(Root.join('db', 'db.sqlite3').to_s)
|
20
|
+
|
21
|
+
elsif defined? PG
|
22
|
+
if ENV['DYNO']
|
23
|
+
# A Postgres connection for Heroku:
|
24
|
+
DB = Sequel.connect(ENV['HEROKU_POSTGRESQL_RED_URL'])
|
25
|
+
else
|
26
|
+
# app name is the same as the root app folder: Root.to_s.split(/\/\\/).last
|
27
|
+
DB = Sequel.connect("postgres://localhost/#{Root.to_s.split(/[\/\\]/).last}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
if defined? Rake
|
31
|
+
##########
|
32
|
+
# start rake segment
|
33
|
+
|
34
|
+
# not yet implemented
|
35
|
+
|
36
|
+
# end rake segment
|
37
|
+
##########
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
|
data/resources/en.yml
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
en:
|
2
|
+
date:
|
3
|
+
abbr_day_names:
|
4
|
+
- Sun
|
5
|
+
- Mon
|
6
|
+
- Tue
|
7
|
+
- Wed
|
8
|
+
- Thu
|
9
|
+
- Fri
|
10
|
+
- Sat
|
11
|
+
abbr_month_names:
|
12
|
+
-
|
13
|
+
- Jan
|
14
|
+
- Feb
|
15
|
+
- Mar
|
16
|
+
- Apr
|
17
|
+
- May
|
18
|
+
- Jun
|
19
|
+
- Jul
|
20
|
+
- Aug
|
21
|
+
- Sep
|
22
|
+
- Oct
|
23
|
+
- Nov
|
24
|
+
- Dec
|
25
|
+
day_names:
|
26
|
+
- Sunday
|
27
|
+
- Monday
|
28
|
+
- Tuesday
|
29
|
+
- Wednesday
|
30
|
+
- Thursday
|
31
|
+
- Friday
|
32
|
+
- Saturday
|
33
|
+
formats:
|
34
|
+
default: ! '%Y-%m-%d'
|
35
|
+
long: ! '%B %d, %Y'
|
36
|
+
short: ! '%b %d'
|
37
|
+
month_names:
|
38
|
+
-
|
39
|
+
- January
|
40
|
+
- February
|
41
|
+
- March
|
42
|
+
- April
|
43
|
+
- May
|
44
|
+
- June
|
45
|
+
- July
|
46
|
+
- August
|
47
|
+
- September
|
48
|
+
- October
|
49
|
+
- November
|
50
|
+
- December
|
51
|
+
order:
|
52
|
+
- :year
|
53
|
+
- :month
|
54
|
+
- :day
|
55
|
+
datetime:
|
56
|
+
distance_in_words:
|
57
|
+
about_x_hours:
|
58
|
+
one: about 1 hour
|
59
|
+
other: about %{count} hours
|
60
|
+
about_x_months:
|
61
|
+
one: about 1 month
|
62
|
+
other: about %{count} months
|
63
|
+
about_x_years:
|
64
|
+
one: about 1 year
|
65
|
+
other: about %{count} years
|
66
|
+
almost_x_years:
|
67
|
+
one: almost 1 year
|
68
|
+
other: almost %{count} years
|
69
|
+
half_a_minute: half a minute
|
70
|
+
less_than_x_minutes:
|
71
|
+
one: less than a minute
|
72
|
+
other: less than %{count} minutes
|
73
|
+
less_than_x_seconds:
|
74
|
+
one: less than 1 second
|
75
|
+
other: less than %{count} seconds
|
76
|
+
over_x_years:
|
77
|
+
one: over 1 year
|
78
|
+
other: over %{count} years
|
79
|
+
x_days:
|
80
|
+
one: 1 day
|
81
|
+
other: ! '%{count} days'
|
82
|
+
x_minutes:
|
83
|
+
one: 1 minute
|
84
|
+
other: ! '%{count} minutes'
|
85
|
+
x_months:
|
86
|
+
one: 1 month
|
87
|
+
other: ! '%{count} months'
|
88
|
+
x_seconds:
|
89
|
+
one: 1 second
|
90
|
+
other: ! '%{count} seconds'
|
91
|
+
prompts:
|
92
|
+
day: Day
|
93
|
+
hour: Hour
|
94
|
+
minute: Minute
|
95
|
+
month: Month
|
96
|
+
second: Seconds
|
97
|
+
year: Year
|
98
|
+
errors:
|
99
|
+
format: ! '%{attribute} %{message}'
|
100
|
+
messages:
|
101
|
+
accepted: must be accepted
|
102
|
+
blank: can't be blank
|
103
|
+
present: must be blank
|
104
|
+
confirmation: ! "doesn't match %{attribute}"
|
105
|
+
empty: can't be empty
|
106
|
+
equal_to: must be equal to %{count}
|
107
|
+
even: must be even
|
108
|
+
exclusion: is reserved
|
109
|
+
greater_than: must be greater than %{count}
|
110
|
+
greater_than_or_equal_to: must be greater than or equal to %{count}
|
111
|
+
inclusion: is not included in the list
|
112
|
+
invalid: is invalid
|
113
|
+
less_than: must be less than %{count}
|
114
|
+
less_than_or_equal_to: must be less than or equal to %{count}
|
115
|
+
not_a_number: is not a number
|
116
|
+
not_an_integer: must be an integer
|
117
|
+
odd: must be odd
|
118
|
+
record_invalid: ! 'Validation failed: %{errors}'
|
119
|
+
restrict_dependent_destroy:
|
120
|
+
one: "Cannot delete record because a dependent %{record} exists"
|
121
|
+
many: "Cannot delete record because dependent %{record} exist"
|
122
|
+
taken: has already been taken
|
123
|
+
too_long:
|
124
|
+
one: is too long (maximum is 1 character)
|
125
|
+
other: is too long (maximum is %{count} characters)
|
126
|
+
too_short:
|
127
|
+
one: is too short (minimum is 1 character)
|
128
|
+
other: is too short (minimum is %{count} characters)
|
129
|
+
wrong_length:
|
130
|
+
one: is the wrong length (should be 1 character)
|
131
|
+
other: is the wrong length (should be %{count} characters)
|
132
|
+
other_than: "must be other than %{count}"
|
133
|
+
template:
|
134
|
+
body: ! 'There were problems with the following fields:'
|
135
|
+
header:
|
136
|
+
one: 1 error prohibited this %{model} from being saved
|
137
|
+
other: ! '%{count} errors prohibited this %{model} from being saved'
|
138
|
+
helpers:
|
139
|
+
select:
|
140
|
+
prompt: Please select
|
141
|
+
submit:
|
142
|
+
create: Create %{model}
|
143
|
+
submit: Save %{model}
|
144
|
+
update: Update %{model}
|
145
|
+
number:
|
146
|
+
currency:
|
147
|
+
format:
|
148
|
+
delimiter: ! ','
|
149
|
+
format: ! '%u%n'
|
150
|
+
precision: 2
|
151
|
+
separator: .
|
152
|
+
significant: false
|
153
|
+
strip_insignificant_zeros: false
|
154
|
+
unit: $
|
155
|
+
format:
|
156
|
+
delimiter: ! ','
|
157
|
+
precision: 3
|
158
|
+
separator: .
|
159
|
+
significant: false
|
160
|
+
strip_insignificant_zeros: false
|
161
|
+
human:
|
162
|
+
decimal_units:
|
163
|
+
format: ! '%n %u'
|
164
|
+
units:
|
165
|
+
billion: Billion
|
166
|
+
million: Million
|
167
|
+
quadrillion: Quadrillion
|
168
|
+
thousand: Thousand
|
169
|
+
trillion: Trillion
|
170
|
+
unit: ''
|
171
|
+
format:
|
172
|
+
delimiter: ''
|
173
|
+
precision: 3
|
174
|
+
significant: true
|
175
|
+
strip_insignificant_zeros: true
|
176
|
+
storage_units:
|
177
|
+
format: ! '%n %u'
|
178
|
+
units:
|
179
|
+
byte:
|
180
|
+
one: Byte
|
181
|
+
other: Bytes
|
182
|
+
gb: GB
|
183
|
+
kb: KB
|
184
|
+
mb: MB
|
185
|
+
tb: TB
|
186
|
+
percentage:
|
187
|
+
format:
|
188
|
+
delimiter: ''
|
189
|
+
format: "%n%"
|
190
|
+
precision:
|
191
|
+
format:
|
192
|
+
delimiter: ''
|
193
|
+
support:
|
194
|
+
array:
|
195
|
+
last_word_connector: ! ', and '
|
196
|
+
two_words_connector: ! ' and '
|
197
|
+
words_connector: ! ', '
|
198
|
+
time:
|
199
|
+
am: am
|
200
|
+
formats:
|
201
|
+
default: ! '%a, %d %b %Y %H:%M:%S %z'
|
202
|
+
long: ! '%B %d, %Y %H:%M'
|
203
|
+
short: ! '%d %b %H:%M'
|
204
|
+
pm: pm
|