1 | /* |
2 | This file is part of the kblog library. |
3 | |
4 | Copyright (c) 2004 Reinhold Kainhofer <reinhold@kainhofer.com> |
5 | Copyright (c) 2006-2007 Christian Weilbach <christian_weilbach@web.de> |
6 | Copyright (c) 2007-2008 Mike McQuaid <mike@mikemcquaid.com> |
7 | |
8 | This library is free software; you can redistribute it and/or |
9 | modify it under the terms of the GNU Library General Public |
10 | License as published by the Free Software Foundation; either |
11 | version 2 of the License, or (at your option) any later version. |
12 | |
13 | This library is distributed in the hope that it will be useful, |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
16 | Library General Public License for more details. |
17 | |
18 | You should have received a copy of the GNU Library General Public License |
19 | along with this library; see the file COPYING.LIB. If not, write to |
20 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
21 | Boston, MA 02110-1301, USA. |
22 | */ |
23 | |
24 | #include "blogger1.h" |
25 | #include "blogger1_p.h" |
26 | #include "blogpost.h" |
27 | |
28 | #include <kxmlrpcclient/client.h> |
29 | |
30 | #include <KDebug> |
31 | #include <KDateTime> |
32 | #include <KLocalizedString> |
33 | |
34 | #include <QList> |
35 | |
36 | #include <QStringList> |
37 | |
38 | using namespace KBlog; |
39 | |
40 | Blogger1::Blogger1( const KUrl &server, QObject *parent ) |
41 | : Blog( server, *new Blogger1Private, parent ) |
42 | { |
43 | kDebug(); |
44 | setUrl( server ); |
45 | } |
46 | |
47 | Blogger1::Blogger1( const KUrl &server, Blogger1Private &dd, QObject *parent ) |
48 | : Blog( server, dd, parent ) |
49 | { |
50 | kDebug(); |
51 | setUrl( server ); |
52 | } |
53 | |
54 | Blogger1::~Blogger1() |
55 | { |
56 | kDebug(); |
57 | } |
58 | |
59 | QString Blogger1::interfaceName() const |
60 | { |
61 | return QLatin1String( "Blogger 1.0" ); |
62 | } |
63 | |
64 | void Blogger1::setUrl( const KUrl &server ) |
65 | { |
66 | Q_D( Blogger1 ); |
67 | Blog::setUrl( server ); |
68 | delete d->mXmlRpcClient; |
69 | d->mXmlRpcClient = new KXmlRpc::Client( server ); |
70 | d->mXmlRpcClient->setUserAgent( userAgent() ); |
71 | } |
72 | |
73 | void Blogger1::fetchUserInfo() |
74 | { |
75 | Q_D( Blogger1 ); |
76 | kDebug() << "Fetch user's info..." ; |
77 | QList<QVariant> args( d->blogger1Args() ); |
78 | d->mXmlRpcClient->call( |
79 | QLatin1String("blogger.getUserInfo" ), args, |
80 | this, SLOT(slotFetchUserInfo(QList<QVariant>,QVariant)), |
81 | this, SLOT(slotError(int,QString,QVariant)) ); |
82 | } |
83 | |
84 | void Blogger1::listBlogs() |
85 | { |
86 | Q_D( Blogger1 ); |
87 | kDebug() << "Fetch List of Blogs..." ; |
88 | QList<QVariant> args( d->blogger1Args() ); |
89 | d->mXmlRpcClient->call( |
90 | QLatin1String("blogger.getUsersBlogs" ), args, |
91 | this, SLOT(slotListBlogs(QList<QVariant>,QVariant)), |
92 | this, SLOT(slotError(int,QString,QVariant)) ); |
93 | } |
94 | |
95 | void Blogger1::listRecentPosts( int number ) |
96 | { |
97 | Q_D( Blogger1 ); |
98 | kDebug() << "Fetching List of Posts..." ; |
99 | QList<QVariant> args( d->defaultArgs( blogId() ) ); |
100 | args << QVariant( number ); |
101 | d->mXmlRpcClient->call( |
102 | d->getCallFromFunction( Blogger1Private::GetRecentPosts ), args, |
103 | this, SLOT(slotListRecentPosts(QList<QVariant>,QVariant)), |
104 | this, SLOT(slotError(int,QString,QVariant)), |
105 | QVariant( number ) ); |
106 | } |
107 | |
108 | void Blogger1::fetchPost( KBlog::BlogPost *post ) |
109 | { |
110 | if ( !post ) { |
111 | kError() << "Blogger1::modifyPost: post is null pointer" ; |
112 | return; |
113 | } |
114 | |
115 | Q_D( Blogger1 ); |
116 | kDebug() << "Fetching Post with url" << post->postId(); |
117 | QList<QVariant> args( d->defaultArgs( post->postId() ) ); |
118 | unsigned int i= d->mCallCounter++; |
119 | d->mCallMap[ i ] = post; |
120 | d->mXmlRpcClient->call( |
121 | d->getCallFromFunction( Blogger1Private::FetchPost ), args, |
122 | this, SLOT(slotFetchPost(QList<QVariant>,QVariant)), |
123 | this, SLOT(slotError(int,QString,QVariant)), |
124 | QVariant( i ) ); |
125 | } |
126 | |
127 | void Blogger1::modifyPost( KBlog::BlogPost *post ) |
128 | { |
129 | Q_D( Blogger1 ); |
130 | |
131 | if ( !post ) { |
132 | kError() << "Blogger1::modifyPost: post is null pointer" ; |
133 | return; |
134 | } |
135 | |
136 | kDebug() << "Uploading Post with postId" << post->postId(); |
137 | unsigned int i= d->mCallCounter++; |
138 | d->mCallMap[ i ] = post; |
139 | QList<QVariant> args( d->defaultArgs( post->postId() ) ); |
140 | d->readArgsFromPost( &args, *post ); |
141 | d->mXmlRpcClient->call( |
142 | d->getCallFromFunction( Blogger1Private::ModifyPost ), args, |
143 | this, SLOT(slotModifyPost(QList<QVariant>,QVariant)), |
144 | this, SLOT(slotError(int,QString,QVariant)), |
145 | QVariant( i ) ); |
146 | } |
147 | |
148 | void Blogger1::createPost( KBlog::BlogPost *post ) |
149 | { |
150 | Q_D( Blogger1 ); |
151 | |
152 | if ( !post ) { |
153 | kError() << "Blogger1::createPost: post is null pointer" ; |
154 | return; |
155 | } |
156 | |
157 | unsigned int i= d->mCallCounter++; |
158 | d->mCallMap[ i ] = post; |
159 | kDebug() << "Creating new Post with blogid" << blogId(); |
160 | QList<QVariant> args( d->defaultArgs( blogId() ) ); |
161 | d->readArgsFromPost( &args, *post ); |
162 | d->mXmlRpcClient->call( |
163 | d->getCallFromFunction( Blogger1Private::CreatePost ), args, |
164 | this, SLOT(slotCreatePost(QList<QVariant>,QVariant)), |
165 | this, SLOT(slotError(int,QString,QVariant)), |
166 | QVariant( i ) ); |
167 | } |
168 | |
169 | void Blogger1::removePost( KBlog::BlogPost *post ) |
170 | { |
171 | Q_D( Blogger1 ); |
172 | |
173 | if ( !post ) { |
174 | kError() << "Blogger1::removePost: post is null pointer" ; |
175 | return; |
176 | } |
177 | |
178 | unsigned int i = d->mCallCounter++; |
179 | d->mCallMap[ i ] = post; |
180 | kDebug() << "Blogger1::removePost: postId=" << post->postId(); |
181 | QList<QVariant> args( d->blogger1Args( post->postId() ) ); |
182 | args << QVariant( true ); // Publish must be set to remove post. |
183 | d->mXmlRpcClient->call( |
184 | QLatin1String("blogger.deletePost" ), args, |
185 | this, SLOT(slotRemovePost(QList<QVariant>,QVariant)), |
186 | this, SLOT(slotError(int,QString,QVariant)), |
187 | QVariant( i ) ); |
188 | } |
189 | |
190 | Blogger1Private::Blogger1Private() : |
191 | mXmlRpcClient(0) |
192 | { |
193 | kDebug(); |
194 | mCallCounter = 1; |
195 | } |
196 | |
197 | Blogger1Private::~Blogger1Private() |
198 | { |
199 | kDebug(); |
200 | delete mXmlRpcClient; |
201 | } |
202 | |
203 | QList<QVariant> Blogger1Private::defaultArgs( const QString &id ) |
204 | { |
205 | kDebug(); |
206 | Q_Q ( Blogger1 ); |
207 | QList<QVariant> args; |
208 | args << QVariant( QLatin1String( "0123456789ABCDEF" ) ); |
209 | if ( !id.isEmpty() ) { |
210 | args << QVariant( id ); |
211 | } |
212 | args << QVariant( q->username() ) |
213 | << QVariant( q->password() ); |
214 | return args; |
215 | } |
216 | |
217 | // reimplemenet defaultArgs, since we may not use it virtually everywhere |
218 | QList<QVariant> Blogger1Private::blogger1Args( const QString &id ) |
219 | { |
220 | kDebug(); |
221 | Q_Q ( Blogger1 ); |
222 | QList<QVariant> args; |
223 | args << QVariant( QLatin1String( "0123456789ABCDEF" ) ); |
224 | if ( !id.isEmpty() ) { |
225 | args << QVariant( id ); |
226 | } |
227 | args << QVariant( q->username() ) |
228 | << QVariant( q->password() ); |
229 | return args; |
230 | } |
231 | |
232 | void Blogger1Private::slotFetchUserInfo( const QList<QVariant> &result, const QVariant &id ) |
233 | { |
234 | Q_Q( Blogger1 ); |
235 | Q_UNUSED( id ); |
236 | |
237 | kDebug(); |
238 | kDebug() << "TOP:" << result[0].typeName(); |
239 | QMap<QString,QString> userInfo; |
240 | if ( result[0].type() != QVariant::Map ) { |
241 | kError() << "Could not fetch user's info out of the result from the server," |
242 | << "not a map." ; |
243 | emit q->error( Blogger1::ParsingError, |
244 | i18n( "Could not fetch user's info out of the result " |
245 | "from the server, not a map." ) ); |
246 | return; |
247 | } |
248 | const QMap<QString,QVariant> resultMap = result[0].toMap(); |
249 | userInfo[QLatin1String("nickname" )]=resultMap[QLatin1String("nickname" )].toString(); |
250 | userInfo[QLatin1String("userid" )]=resultMap[QLatin1String("userid" )].toString(); |
251 | userInfo[QLatin1String("url" )]=resultMap[QLatin1String("url" )].toString(); |
252 | userInfo[QLatin1String("email" )]=resultMap[QLatin1String("email" )].toString(); |
253 | userInfo[QLatin1String("lastname" )]=resultMap[QLatin1String("lastname" )].toString(); |
254 | userInfo[QLatin1String("firstname" )]=resultMap[QLatin1String("firstname" )].toString(); |
255 | |
256 | emit q->fetchedUserInfo( userInfo ); |
257 | } |
258 | |
259 | void Blogger1Private::slotListBlogs( const QList<QVariant> &result, const QVariant &id ) |
260 | { |
261 | Q_Q( Blogger1 ); |
262 | Q_UNUSED( id ); |
263 | |
264 | kDebug(); |
265 | kDebug() << "TOP:" << result[0].typeName(); |
266 | QList<QMap<QString,QString> > blogsList; |
267 | if ( result[0].type() != QVariant::List ) { |
268 | kError() << "Could not fetch blogs out of the result from the server," |
269 | << "not a list." ; |
270 | emit q->error( Blogger1::ParsingError, |
271 | i18n( "Could not fetch blogs out of the result " |
272 | "from the server, not a list." ) ); |
273 | return; |
274 | } |
275 | const QList<QVariant> posts = result[0].toList(); |
276 | QList<QVariant>::ConstIterator it = posts.begin(); |
277 | QList<QVariant>::ConstIterator end = posts.end(); |
278 | for ( ; it != end; ++it ) { |
279 | kDebug() << "MIDDLE:" << ( *it ).typeName(); |
280 | const QMap<QString, QVariant> postInfo = ( *it ).toMap(); |
281 | QMap<QString,QString> blogInfo; |
282 | blogInfo[ QLatin1String("id" ) ] = postInfo[QLatin1String("blogid" )].toString(); |
283 | blogInfo[ QLatin1String("url" ) ] = postInfo[QLatin1String("url" )].toString(); |
284 | blogInfo[ QLatin1String("apiUrl" ) ] = postInfo[QLatin1String("xmlrpc" )].toString(); |
285 | blogInfo[ QLatin1String("title" ) ] = postInfo[QLatin1String("blogName" )].toString(); |
286 | kDebug() << "Blog information retrieved: ID =" << blogInfo[QLatin1String("id" )] |
287 | << ", Name =" << blogInfo[QLatin1String("title" )]; |
288 | blogsList << blogInfo; |
289 | } |
290 | emit q->listedBlogs( blogsList ); |
291 | } |
292 | |
293 | void Blogger1Private::slotListRecentPosts( const QList<QVariant> &result, const QVariant &id ) |
294 | { |
295 | Q_Q( Blogger1 ); |
296 | int count = id.toInt(); // not sure if needed, actually the API should |
297 | // not give more posts |
298 | |
299 | kDebug(); |
300 | kDebug() << "TOP:" << result[0].typeName(); |
301 | |
302 | QList <BlogPost> fetchedPostList; |
303 | |
304 | if ( result[0].type() != QVariant::List ) { |
305 | kError() << "Could not fetch list of posts out of the" |
306 | << "result from the server, not a list." ; |
307 | emit q->error( Blogger1::ParsingError, |
308 | i18n( "Could not fetch list of posts out of the result " |
309 | "from the server, not a list." ) ); |
310 | return; |
311 | } |
312 | const QList<QVariant> postReceived = result[0].toList(); |
313 | QList<QVariant>::ConstIterator it = postReceived.begin(); |
314 | QList<QVariant>::ConstIterator end = postReceived.end(); |
315 | for ( ; it != end; ++it ) { |
316 | BlogPost post; |
317 | kDebug() << "MIDDLE:" << ( *it ).typeName(); |
318 | const QMap<QString, QVariant> postInfo = ( *it ).toMap(); |
319 | if ( readPostFromMap( &post, postInfo ) ) { |
320 | kDebug() << "Post with ID:" |
321 | << post.postId() |
322 | << "appended in fetchedPostList" ; |
323 | post.setStatus( BlogPost::Fetched ); |
324 | fetchedPostList.append( post ); |
325 | } else { |
326 | kError() << "readPostFromMap failed!" ; |
327 | emit q->error( Blogger1::ParsingError, i18n( "Could not read post." ) ); |
328 | } |
329 | if ( --count == 0 ) { |
330 | break; |
331 | } |
332 | } |
333 | kDebug() << "Emitting listRecentPostsFinished()" ; |
334 | emit q->listedRecentPosts( fetchedPostList ); |
335 | } |
336 | |
337 | void Blogger1Private::slotFetchPost( const QList<QVariant> &result, const QVariant &id ) |
338 | { |
339 | Q_Q( Blogger1 ); |
340 | kDebug(); |
341 | |
342 | KBlog::BlogPost *post = mCallMap[ id.toInt() ]; |
343 | mCallMap.remove( id.toInt() ); |
344 | |
345 | //array of structs containing ISO.8601 |
346 | // dateCreated, String userid, String postid, String content; |
347 | // TODO: Time zone for the dateCreated! |
348 | kDebug () << "TOP:" << result[0].typeName(); |
349 | if ( result[0].type() == QVariant::Map && |
350 | readPostFromMap( post, result[0].toMap() ) ) { |
351 | kDebug() << "Emitting fetchedPost()" ; |
352 | post->setStatus( KBlog::BlogPost::Fetched ); |
353 | emit q->fetchedPost( post ); |
354 | } else { |
355 | kError() << "Could not fetch post out of the result from the server." ; |
356 | post->setError( i18n( "Could not fetch post out of the result from the server." ) ); |
357 | post->setStatus( BlogPost::Error ); |
358 | emit q->errorPost( Blogger1::ParsingError, |
359 | i18n( "Could not fetch post out of the result from the server." ), post ); |
360 | } |
361 | } |
362 | |
363 | void Blogger1Private::slotCreatePost( const QList<QVariant> &result, const QVariant &id ) |
364 | { |
365 | Q_Q( Blogger1 ); |
366 | KBlog::BlogPost *post = mCallMap[ id.toInt() ]; |
367 | mCallMap.remove( id.toInt() ); |
368 | |
369 | kDebug(); |
370 | //array of structs containing ISO.8601 |
371 | // dateCreated, String userid, String postid, String content; |
372 | // TODO: Time zone for the dateCreated! |
373 | kDebug () << "TOP:" << result[0].typeName(); |
374 | if ( result[0].type() != QVariant::String && |
375 | result[0].type() != QVariant::Int ) { |
376 | kError() << "Could not read the postId, not a string or an integer." ; |
377 | emit q->errorPost( Blogger1::ParsingError, |
378 | i18n( "Could not read the postId, not a string or an integer." ), |
379 | post ); |
380 | return; |
381 | } |
382 | QString serverID; |
383 | if ( result[0].type() == QVariant::String ) { |
384 | serverID = result[0].toString(); |
385 | } else if ( result[0].type() == QVariant::Int ) { |
386 | serverID = QString::fromLatin1( "%1" ).arg( result[0].toInt() ); |
387 | } |
388 | post->setPostId( serverID ); |
389 | post->setStatus( KBlog::BlogPost::Created ); |
390 | kDebug() << "emitting createdPost()" |
391 | << "for title: \"" << post->title() |
392 | << "\" server id: " << serverID; |
393 | emit q->createdPost( post ); |
394 | } |
395 | |
396 | void Blogger1Private::slotModifyPost( const QList<QVariant> &result, const QVariant &id ) |
397 | { |
398 | Q_Q( Blogger1 ); |
399 | KBlog::BlogPost *post = mCallMap[ id.toInt() ]; |
400 | mCallMap.remove( id.toInt() ); |
401 | |
402 | kDebug(); |
403 | //array of structs containing ISO.8601 |
404 | // dateCreated, String userid, String postid, String content; |
405 | // TODO: Time zone for the dateCreated! |
406 | kDebug() << "TOP:" << result[0].typeName(); |
407 | if ( result[0].type() != QVariant::Bool && |
408 | result[0].type() != QVariant::Int ) { |
409 | kError() << "Could not read the result, not a boolean." ; |
410 | emit q->errorPost( Blogger1::ParsingError, |
411 | i18n( "Could not read the result, not a boolean." ), |
412 | post ); |
413 | return; |
414 | } |
415 | post->setStatus( KBlog::BlogPost::Modified ); |
416 | kDebug() << "emitting modifiedPost() for title: \"" |
417 | << post->title() << "\"" ; |
418 | emit q->modifiedPost( post ); |
419 | } |
420 | |
421 | void Blogger1Private::slotRemovePost( const QList<QVariant> &result, const QVariant &id ) |
422 | { |
423 | Q_Q( Blogger1 ); |
424 | KBlog::BlogPost *post = mCallMap[ id.toInt() ]; |
425 | mCallMap.remove( id.toInt() ); |
426 | |
427 | kDebug() << "slotRemovePost" ; |
428 | //array of structs containing ISO.8601 |
429 | // dateCreated, String userid, String postid, String content; |
430 | // TODO: Time zone for the dateCreated! |
431 | kDebug() << "TOP:" << result[0].typeName(); |
432 | if ( result[0].type() != QVariant::Bool && |
433 | result[0].type() != QVariant::Int ) { |
434 | kError() << "Could not read the result, not a boolean." ; |
435 | emit q->errorPost( Blogger1::ParsingError, |
436 | i18n( "Could not read the result, not a boolean." ), |
437 | post ); |
438 | return; |
439 | } |
440 | post->setStatus( KBlog::BlogPost::Removed ); |
441 | kDebug() << "emitting removedPost()" ; |
442 | emit q->removedPost( post ); |
443 | } |
444 | |
445 | void Blogger1Private::slotError( int number, |
446 | const QString &errorString, |
447 | const QVariant &id ) |
448 | { |
449 | Q_Q( Blogger1 ); |
450 | Q_UNUSED( number ); |
451 | kDebug() << "An error occurred: " << errorString; |
452 | BlogPost *post = mCallMap[ id.toInt() ]; |
453 | |
454 | if ( post ) |
455 | emit q->errorPost( Blogger1::XmlRpc, errorString, post ); |
456 | else |
457 | emit q->error( Blogger1::XmlRpc, errorString ); |
458 | } |
459 | |
460 | bool Blogger1Private::readPostFromMap( |
461 | BlogPost *post, const QMap<QString, QVariant> &postInfo ) |
462 | { |
463 | // FIXME: integrate error handling |
464 | if ( !post ) { |
465 | return false; |
466 | } |
467 | QStringList mapkeys = postInfo.keys(); |
468 | kDebug() << endl << "Keys:" << mapkeys.join( QLatin1String(", " ) ); |
469 | kDebug() << endl; |
470 | |
471 | KDateTime dt( postInfo[QLatin1String("dateCreated" )].toDateTime(), KDateTime::UTC ); |
472 | if ( dt.isValid() && !dt.isNull() ) { |
473 | post->setCreationDateTime( dt.toLocalZone() ); |
474 | } |
475 | dt = KDateTime ( postInfo[QLatin1String("lastModified" )].toDateTime(), KDateTime::UTC ); |
476 | if ( dt.isValid() && !dt.isNull() ) { |
477 | post->setModificationDateTime( dt.toLocalZone() ); |
478 | } |
479 | post->setPostId( postInfo[QLatin1String("postid" )].toString().isEmpty() ? postInfo[QLatin1String("postId" )].toString() : |
480 | postInfo[QLatin1String("postid" )].toString() ); |
481 | |
482 | QString title( postInfo[QLatin1String("title" )].toString() ); |
483 | //QString description( postInfo["description"].toString() ); |
484 | QString contents; |
485 | if ( postInfo[QLatin1String("content" )].type() == QVariant::ByteArray ) { |
486 | QByteArray tmpContent = postInfo[QLatin1String("content" )].toByteArray(); |
487 | contents = QString::fromUtf8( tmpContent.data(), tmpContent.size() ); |
488 | } else { |
489 | contents = postInfo[QLatin1String("content" )].toString(); |
490 | } |
491 | QStringList category; |
492 | |
493 | // Check for hacked title/category support (e.g. in Wordpress) |
494 | QRegExp titleMatch = QRegExp( QLatin1String("<title>([^<]*)</title>" ) ); |
495 | QRegExp categoryMatch = QRegExp( QLatin1String("<category>([^<]*)</category>" ) ); |
496 | if ( contents.indexOf( titleMatch ) != -1 ) { |
497 | // Get the title value from the regular expression match |
498 | title = titleMatch.cap( 1 ); |
499 | } |
500 | if ( contents.indexOf( categoryMatch ) != -1 ) { |
501 | // Get the category value from the regular expression match |
502 | category = categoryMatch.capturedTexts(); |
503 | } |
504 | contents.remove( titleMatch ); |
505 | contents.remove( categoryMatch ); |
506 | |
507 | post->setTitle( title ); |
508 | post->setContent( contents ); |
509 | post->setCategories( category ); |
510 | return true; |
511 | } |
512 | |
513 | bool Blogger1Private::readArgsFromPost( QList<QVariant> *args, const BlogPost &post ) |
514 | { |
515 | if ( !args ) { |
516 | return false; |
517 | } |
518 | const QStringList categories = post.categories(); |
519 | QString content = QLatin1String("<title>" ) + post.title() + QLatin1String("</title>" ); |
520 | QStringList::const_iterator it; |
521 | QStringList::const_iterator end(categories.constEnd()); |
522 | for ( it = categories.constBegin(); it != end; ++it ) { |
523 | content += QLatin1String("<category>" ) + *it + QLatin1String("</category>" ); |
524 | } |
525 | content += post.content(); |
526 | *args << QVariant( content ); |
527 | *args << QVariant( !post.isPrivate() ); |
528 | return true; |
529 | } |
530 | |
531 | QString Blogger1Private::getCallFromFunction( FunctionToCall type ) |
532 | { |
533 | switch ( type ) { |
534 | case GetRecentPosts: return QLatin1String("blogger.getRecentPosts" ); |
535 | case CreatePost: return QLatin1String("blogger.newPost" ); |
536 | case ModifyPost: return QLatin1String("blogger.editPost" ); |
537 | case FetchPost: return QLatin1String("blogger.getPost" ); |
538 | default: return QString(); |
539 | } |
540 | } |
541 | |
542 | #include "moc_blogger1.cpp" |
543 | |