From 70367565ebba486c48e9c56bbaa19145b45c86f7 Mon Sep 17 00:00:00 2001
From: Liang Ding
Date: Fri, 29 Nov 2019 19:15:12 +0800
Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=88=9D=E5=A7=8B=E5=8C=96?=
=?UTF-8?q?=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.travis.yml | 6 +
CHANGE_LOGS.html | 594 ++
Dockerfile | 18 +
LICENSE | 661 ++
README.md | 17 +-
THIRD_PARTY_LICENSE | 46 +
gulpfile.js | 117 +
package-lock.json | 6179 +++++++++++++++++
package.json | 42 +
pom.xml | 413 ++
src/assembly/bin.xml | 32 +
src/main/java/org/b3log/symphony/Server.java | 244 +
.../b3log/symphony/cache/ArticleCache.java | 306 +
.../b3log/symphony/cache/CommentCache.java | 75 +
.../org/b3log/symphony/cache/DomainCache.java | 97 +
.../org/b3log/symphony/cache/OptionCache.java | 75 +
.../org/b3log/symphony/cache/TagCache.java | 304 +
.../org/b3log/symphony/cache/UserCache.java | 127 +
.../event/ArticleAddAudioHandler.java | 69 +
.../symphony/event/ArticleAddNotifier.java | 218 +
.../symphony/event/ArticleBaiduSender.java | 120 +
.../symphony/event/ArticleSearchAdder.java | 89 +
.../symphony/event/ArticleSearchUpdater.java | 89 +
.../event/ArticleUpdateAudioHandler.java | 69 +
.../symphony/event/ArticleUpdateNotifier.java | 170 +
.../b3log/symphony/event/CommentNotifier.java | 384 +
.../symphony/event/CommentUpdateNotifier.java | 140 +
.../org/b3log/symphony/event/EventTypes.java | 54 +
.../org/b3log/symphony/model/Article.java | 529 ++
.../org/b3log/symphony/model/Breezemoon.java | 107 +
.../org/b3log/symphony/model/Character.java | 100 +
.../org/b3log/symphony/model/Comment.java | 295 +
.../java/org/b3log/symphony/model/Common.java | 849 +++
.../java/org/b3log/symphony/model/Domain.java | 147 +
.../org/b3log/symphony/model/Emotion.java | 77 +
.../java/org/b3log/symphony/model/Follow.java | 80 +
.../org/b3log/symphony/model/Invitecode.java | 84 +
.../java/org/b3log/symphony/model/Link.java | 160 +
.../org/b3log/symphony/model/Liveness.java | 147 +
.../b3log/symphony/model/Notification.java | 236 +
.../org/b3log/symphony/model/Operation.java | 345 +
.../java/org/b3log/symphony/model/Option.java | 151 +
.../org/b3log/symphony/model/Permission.java | 525 ++
.../b3log/symphony/model/Pointtransfer.java | 399 ++
.../org/b3log/symphony/model/Referral.java | 79 +
.../java/org/b3log/symphony/model/Report.java | 178 +
.../org/b3log/symphony/model/Revision.java | 69 +
.../java/org/b3log/symphony/model/Reward.java | 85 +
.../java/org/b3log/symphony/model/Role.java | 96 +
.../java/org/b3log/symphony/model/Tag.java | 505 ++
.../org/b3log/symphony/model/UserExt.java | 691 ++
.../org/b3log/symphony/model/Verifycode.java | 112 +
.../java/org/b3log/symphony/model/Visit.java | 84 +
.../java/org/b3log/symphony/model/Vote.java | 96 +
.../symphony/model/feed/RSSCategory.java | 64 +
.../b3log/symphony/model/feed/RSSChannel.java | 339 +
.../b3log/symphony/model/feed/RSSItem.java | 283 +
.../b3log/symphony/model/sitemap/Sitemap.java | 268 +
.../symphony/processor/ActivityProcessor.java | 531 ++
.../symphony/processor/AdminProcessor.java | 2654 +++++++
.../processor/AfterRequestHandler.java | 72 +
.../symphony/processor/ArticleProcessor.java | 1320 ++++
.../processor/BeforeRequestHandler.java | 164 +
.../processor/BreezemoonProcessor.java | 302 +
.../symphony/processor/CaptchaProcessor.java | 230 +
.../symphony/processor/ChargeProcessor.java | 78 +
.../symphony/processor/ChatroomProcessor.java | 213 +
.../symphony/processor/CityProcessor.java | 284 +
.../symphony/processor/CommentProcessor.java | 539 ++
.../symphony/processor/DomainProcessor.java | 181 +
.../symphony/processor/ErrorProcessor.java | 110 +
.../symphony/processor/FeedProcessor.java | 210 +
.../processor/FetchUploadProcessor.java | 174 +
.../processor/FileUploadProcessor.java | 270 +
.../symphony/processor/FollowProcessor.java | 354 +
.../symphony/processor/ForwardProcessor.java | 109 +
.../symphony/processor/IndexProcessor.java | 475 ++
.../symphony/processor/LoginProcessor.java | 758 ++
.../processor/NotificationProcessor.java | 766 ++
.../symphony/processor/ReportProcessor.java | 114 +
.../symphony/processor/SearchProcessor.java | 205 +
.../symphony/processor/SettingsProcessor.java | 1159 ++++
.../symphony/processor/SitemapProcessor.java | 83 +
.../symphony/processor/SkinRenderer.java | 174 +
.../processor/StatisticProcessor.java | 240 +
.../symphony/processor/TagProcessor.java | 255 +
.../symphony/processor/TopProcessor.java | 190 +
.../symphony/processor/UserProcessor.java | 994 +++
.../symphony/processor/VoteProcessor.java | 317 +
.../processor/advice/AnonymousViewCheck.java | 208 +
.../symphony/processor/advice/CSRFCheck.java | 75 +
.../symphony/processor/advice/CSRFToken.java | 54 +
.../symphony/processor/advice/LoginCheck.java | 85 +
.../processor/advice/PermissionCheck.java | 109 +
.../processor/advice/PermissionGrant.java | 88 +
.../processor/advice/UserBlockCheck.java | 86 +
.../advice/stopwatch/StopwatchEndAdvice.java | 62 +
.../stopwatch/StopwatchStartAdvice.java | 40 +
.../Activity1A0001CollectValidation.java | 95 +
.../validate/Activity1A0001Validation.java | 131 +
.../advice/validate/ArticleAddValidation.java | 215 +
.../validate/ArticleUpdateValidation.java | 41 +
.../advice/validate/ChatMsgAddValidation.java | 97 +
.../advice/validate/CommentAddValidation.java | 102 +
.../validate/CommentUpdateValidation.java | 110 +
.../validate/PointTransferValidation.java | 118 +
.../validate/ShowArticleUpdateValidation.java | 76 +
.../validate/UpdatePasswordValidation.java | 70 +
.../validate/UpdateProfilesValidation.java | 184 +
.../validate/UserForgetPwdValidation.java | 83 +
.../validate/UserRegister2Validation.java | 120 +
.../validate/UserRegisterValidation.java | 256 +
.../processor/channel/ArticleChannel.java | 295 +
.../processor/channel/ArticleListChannel.java | 118 +
.../processor/channel/ChatroomChannel.java | 139 +
.../processor/channel/GobangChannel.java | 594 ++
.../processor/channel/UserChannel.java | 182 +
.../repository/ArticleRepository.java | 141 +
.../repository/BreezemoonRepository.java | 40 +
.../repository/CharacterRepository.java | 39 +
.../repository/CommentRepository.java | 184 +
.../symphony/repository/DomainRepository.java | 83 +
.../repository/DomainTagRepository.java | 118 +
.../repository/EmotionRepository.java | 84 +
.../symphony/repository/FollowRepository.java | 136 +
.../repository/InvitecodeRepository.java | 40 +
.../symphony/repository/LinkRepository.java | 79 +
.../repository/LivenessRepository.java | 76 +
.../repository/NotificationRepository.java | 88 +
.../repository/OperationRepository.java | 40 +
.../symphony/repository/OptionRepository.java | 83 +
.../repository/PermissionRepository.java | 40 +
.../repository/PointtransferRepository.java | 84 +
.../repository/ReferralRepository.java | 65 +
.../symphony/repository/ReportRepository.java | 40 +
.../repository/RevisionRepository.java | 40 +
.../symphony/repository/RewardRepository.java | 51 +
.../repository/RolePermissionRepository.java | 79 +
.../symphony/repository/RoleRepository.java | 40 +
.../repository/TagArticleRepository.java | 109 +
.../symphony/repository/TagRepository.java | 196 +
.../symphony/repository/TagTagRepository.java | 147 +
.../symphony/repository/UserRepository.java | 155 +
.../repository/UserRoleRepository.java | 41 +
.../repository/UserTagRepository.java | 123 +
.../repository/VerifycodeRepository.java | 40 +
.../symphony/repository/VisitRepository.java | 50 +
.../symphony/repository/VoteRepository.java | 84 +
.../symphony/service/ActivityMgmtService.java | 579 ++
.../service/ActivityQueryService.java | 294 +
.../symphony/service/ArticleMgmtService.java | 1818 +++++
.../symphony/service/ArticleQueryService.java | 2233 ++++++
.../symphony/service/AudioMgmtService.java | 240 +
.../symphony/service/AvatarQueryService.java | 178 +
.../service/BreezemoonMgmtService.java | 185 +
.../service/BreezemoonQueryService.java | 393 ++
.../symphony/service/CacheMgmtService.java | 181 +
.../service/CharacterQueryService.java | 303 +
.../symphony/service/CommentMgmtService.java | 707 ++
.../symphony/service/CommentQueryService.java | 888 +++
.../symphony/service/CronMgmtService.java | 174 +
.../symphony/service/DataModelService.java | 506 ++
.../symphony/service/DomainMgmtService.java | 216 +
.../symphony/service/DomainQueryService.java | 382 +
.../symphony/service/EmotionMgmtService.java | 101 +
.../symphony/service/EmotionQueryService.java | 75 +
.../symphony/service/FollowMgmtService.java | 310 +
.../symphony/service/FollowQueryService.java | 529 ++
.../symphony/service/InitMgmtService.java | 682 ++
.../service/InvitecodeMgmtService.java | 186 +
.../service/InvitecodeQueryService.java | 225 +
.../symphony/service/LinkMgmtService.java | 236 +
.../symphony/service/LinkQueryService.java | 87 +
.../symphony/service/LivenessMgmtService.java | 95 +
.../service/LivenessQueryService.java | 101 +
.../symphony/service/MailMgmtService.java | 223 +
.../service/NotificationMgmtService.java | 908 +++
.../service/NotificationQueryService.java | 1278 ++++
.../service/OperationMgmtService.java | 62 +
.../service/OperationQueryService.java | 154 +
.../symphony/service/OptionMgmtService.java | 113 +
.../symphony/service/OptionQueryService.java | 280 +
.../service/PointtransferMgmtService.java | 129 +
.../service/PointtransferQueryService.java | 613 ++
.../symphony/service/PostExportService.java | 230 +
.../symphony/service/ReferralMgmtService.java | 90 +
.../symphony/service/ReportMgmtService.java | 146 +
.../symphony/service/ReportQueryService.java | 271 +
.../service/RevisionQueryService.java | 236 +
.../symphony/service/RewardMgmtService.java | 74 +
.../symphony/service/RewardQueryService.java | 101 +
.../symphony/service/RoleMgmtService.java | 136 +
.../symphony/service/RoleQueryService.java | 417 ++
.../symphony/service/SearchMgmtService.java | 267 +
.../symphony/service/SearchQueryService.java | 176 +
.../service/ShortLinkQueryService.java | 142 +
.../symphony/service/SitemapQueryService.java | 134 +
.../symphony/service/TagMgmtService.java | 397 ++
.../symphony/service/TagQueryService.java | 696 ++
.../symphony/service/UserMgmtService.java | 862 +++
.../symphony/service/UserQueryService.java | 736 ++
.../service/VerifycodeMgmtService.java | 209 +
.../service/VerifycodeQueryService.java | 105 +
.../symphony/service/VisitMgmtService.java | 111 +
.../symphony/service/VoteMgmtService.java | 369 +
.../symphony/service/VoteQueryService.java | 140 +
.../org/b3log/symphony/util/Emotions.java | 1009 +++
.../java/org/b3log/symphony/util/Escapes.java | 75 +
.../java/org/b3log/symphony/util/Geos.java | 165 +
.../org/b3log/symphony/util/Gravatars.java | 81 +
.../java/org/b3log/symphony/util/Headers.java | 92 +
.../java/org/b3log/symphony/util/Images.java | 70 +
.../java/org/b3log/symphony/util/JSONs.java | 60 +
.../org/b3log/symphony/util/Languages.java | 55 +
.../java/org/b3log/symphony/util/Links.java | 180 +
.../org/b3log/symphony/util/MP3Players.java | 73 +
.../java/org/b3log/symphony/util/Mails.java | 619 ++
.../org/b3log/symphony/util/Markdowns.java | 461 ++
.../org/b3log/symphony/util/Networks.java | 71 +
.../java/org/b3log/symphony/util/Pangu.java | 144 +
.../java/org/b3log/symphony/util/Results.java | 55 +
.../java/org/b3log/symphony/util/Runes.java | 77 +
.../org/b3log/symphony/util/Sessions.java | 513 ++
.../org/b3log/symphony/util/StatusCodes.java | 47 +
.../org/b3log/symphony/util/Symphonys.java | 1040 +++
.../org/b3log/symphony/util/Templates.java | 91 +
.../org/b3log/symphony/util/Tesseracts.java | 57 +
.../org/b3log/symphony/util/VideoPlayers.java | 77 +
src/main/resources/CHANGE_LOGS.html | 594 ++
src/main/resources/css/base.css | 1 +
src/main/resources/css/error.css | 1 +
src/main/resources/css/home.css | 1 +
src/main/resources/css/index.css | 1 +
src/main/resources/css/mobile-base.css | 1 +
src/main/resources/css/responsive.css | 1 +
src/main/resources/css/selection.json | 1959 ++++++
src/main/resources/docker/latke.properties | 35 +
src/main/resources/docker/local.properties | 36 +
src/main/resources/emoji/ZeroClipboard.swf | Bin 0 -> 6580 bytes
src/main/resources/emoji/favicon.ico | Bin 0 -> 32988 bytes
src/main/resources/emoji/graphics/+1.png | Bin 0 -> 1098 bytes
src/main/resources/emoji/graphics/-1.png | Bin 0 -> 1090 bytes
src/main/resources/emoji/graphics/100.png | Bin 0 -> 1388 bytes
src/main/resources/emoji/graphics/1234.png | Bin 0 -> 1054 bytes
src/main/resources/emoji/graphics/263a.png | Bin 0 -> 1111 bytes
src/main/resources/emoji/graphics/8ball.png | Bin 0 -> 1052 bytes
src/main/resources/emoji/graphics/a.png | Bin 0 -> 806 bytes
src/main/resources/emoji/graphics/ab.png | Bin 0 -> 973 bytes
src/main/resources/emoji/graphics/abc.png | Bin 0 -> 928 bytes
src/main/resources/emoji/graphics/abcd.png | Bin 0 -> 1111 bytes
src/main/resources/emoji/graphics/accept.png | Bin 0 -> 928 bytes
.../emoji/graphics/aerial_tramway.png | Bin 0 -> 730 bytes
.../resources/emoji/graphics/airplane.png | Bin 0 -> 1121 bytes
.../resources/emoji/graphics/alarm_clock.png | Bin 0 -> 1224 bytes
src/main/resources/emoji/graphics/alien.png | Bin 0 -> 1094 bytes
.../resources/emoji/graphics/ambulance.png | Bin 0 -> 825 bytes
src/main/resources/emoji/graphics/anchor.png | Bin 0 -> 948 bytes
src/main/resources/emoji/graphics/angel.png | Bin 0 -> 1131 bytes
src/main/resources/emoji/graphics/anger.png | Bin 0 -> 1145 bytes
src/main/resources/emoji/graphics/angry.png | Bin 0 -> 985 bytes
.../resources/emoji/graphics/anguished.png | Bin 0 -> 1025 bytes
src/main/resources/emoji/graphics/ant.png | Bin 0 -> 1083 bytes
src/main/resources/emoji/graphics/apple.png | Bin 0 -> 846 bytes
.../resources/emoji/graphics/aquarius.png | Bin 0 -> 675 bytes
src/main/resources/emoji/graphics/aries.png | Bin 0 -> 976 bytes
.../emoji/graphics/arrow_backward.png | Bin 0 -> 457 bytes
.../emoji/graphics/arrow_double_down.png | Bin 0 -> 605 bytes
.../emoji/graphics/arrow_double_up.png | Bin 0 -> 595 bytes
.../resources/emoji/graphics/arrow_down.png | Bin 0 -> 601 bytes
.../emoji/graphics/arrow_down_small.png | Bin 0 -> 514 bytes
.../emoji/graphics/arrow_forward.png | Bin 0 -> 456 bytes
.../emoji/graphics/arrow_heading_down.png | Bin 0 -> 673 bytes
.../emoji/graphics/arrow_heading_up.png | Bin 0 -> 671 bytes
.../resources/emoji/graphics/arrow_left.png | Bin 0 -> 604 bytes
.../emoji/graphics/arrow_lower_left.png | Bin 0 -> 536 bytes
.../emoji/graphics/arrow_lower_right.png | Bin 0 -> 529 bytes
.../resources/emoji/graphics/arrow_right.png | Bin 0 -> 538 bytes
.../emoji/graphics/arrow_right_hook.png | Bin 0 -> 722 bytes
.../resources/emoji/graphics/arrow_up.png | Bin 0 -> 595 bytes
.../emoji/graphics/arrow_up_down.png | Bin 0 -> 633 bytes
.../emoji/graphics/arrow_up_small.png | Bin 0 -> 490 bytes
.../emoji/graphics/arrow_upper_left.png | Bin 0 -> 538 bytes
.../emoji/graphics/arrow_upper_right.png | Bin 0 -> 538 bytes
.../emoji/graphics/arrows_clockwise.png | Bin 0 -> 816 bytes
.../graphics/arrows_counterclockwise.png | Bin 0 -> 1023 bytes
src/main/resources/emoji/graphics/art.png | Bin 0 -> 1035 bytes
.../emoji/graphics/articulated_lorry.png | Bin 0 -> 738 bytes
.../resources/emoji/graphics/astonished.png | Bin 0 -> 1113 bytes
src/main/resources/emoji/graphics/atm.png | Bin 0 -> 943 bytes
src/main/resources/emoji/graphics/b.png | Bin 0 -> 706 bytes
src/main/resources/emoji/graphics/baby.png | Bin 0 -> 991 bytes
.../resources/emoji/graphics/baby_bottle.png | Bin 0 -> 762 bytes
.../resources/emoji/graphics/baby_chick.png | Bin 0 -> 950 bytes
.../resources/emoji/graphics/baby_symbol.png | Bin 0 -> 917 bytes
src/main/resources/emoji/graphics/back.png | Bin 0 -> 1119 bytes
.../emoji/graphics/baggage_claim.png | Bin 0 -> 586 bytes
src/main/resources/emoji/graphics/balloon.png | Bin 0 -> 771 bytes
.../emoji/graphics/ballot_box_with_check.png | Bin 0 -> 668 bytes
src/main/resources/emoji/graphics/bamboo.png | Bin 0 -> 882 bytes
src/main/resources/emoji/graphics/banana.png | Bin 0 -> 1171 bytes
.../resources/emoji/graphics/bangbang.png | Bin 0 -> 282 bytes
src/main/resources/emoji/graphics/bank.png | Bin 0 -> 682 bytes
.../resources/emoji/graphics/bar_chart.png | Bin 0 -> 663 bytes
src/main/resources/emoji/graphics/barber.png | Bin 0 -> 682 bytes
.../resources/emoji/graphics/baseball.png | Bin 0 -> 1361 bytes
.../resources/emoji/graphics/basketball.png | Bin 0 -> 955 bytes
src/main/resources/emoji/graphics/bath.png | Bin 0 -> 808 bytes
src/main/resources/emoji/graphics/bathtub.png | Bin 0 -> 735 bytes
src/main/resources/emoji/graphics/battery.png | Bin 0 -> 579 bytes
src/main/resources/emoji/graphics/bear.png | Bin 0 -> 1148 bytes
src/main/resources/emoji/graphics/bee.png | Bin 0 -> 1064 bytes
src/main/resources/emoji/graphics/beer.png | Bin 0 -> 764 bytes
src/main/resources/emoji/graphics/beers.png | Bin 0 -> 878 bytes
src/main/resources/emoji/graphics/beetle.png | Bin 0 -> 1181 bytes
.../resources/emoji/graphics/beginner.png | Bin 0 -> 495 bytes
src/main/resources/emoji/graphics/bell.png | Bin 0 -> 743 bytes
src/main/resources/emoji/graphics/bento.png | Bin 0 -> 1110 bytes
.../resources/emoji/graphics/bicyclist.png | Bin 0 -> 1279 bytes
src/main/resources/emoji/graphics/bike.png | Bin 0 -> 1130 bytes
src/main/resources/emoji/graphics/bikini.png | Bin 0 -> 977 bytes
src/main/resources/emoji/graphics/bird.png | Bin 0 -> 1058 bytes
.../resources/emoji/graphics/birthday.png | Bin 0 -> 1210 bytes
.../resources/emoji/graphics/black_circle.png | Bin 0 -> 756 bytes
.../resources/emoji/graphics/black_joker.png | Bin 0 -> 973 bytes
.../emoji/graphics/black_large_square.png | Bin 0 -> 340 bytes
.../graphics/black_medium_small_square.png | Bin 0 -> 236 bytes
.../emoji/graphics/black_medium_square.png | Bin 0 -> 264 bytes
.../resources/emoji/graphics/black_nib.png | Bin 0 -> 773 bytes
.../emoji/graphics/black_small_square.png | Bin 0 -> 167 bytes
.../emoji/graphics/black_square_button.png | Bin 0 -> 449 bytes
src/main/resources/emoji/graphics/blossom.png | Bin 0 -> 1020 bytes
.../resources/emoji/graphics/blowfish.png | Bin 0 -> 917 bytes
.../resources/emoji/graphics/blue_book.png | Bin 0 -> 665 bytes
.../resources/emoji/graphics/blue_car.png | Bin 0 -> 779 bytes
.../resources/emoji/graphics/blue_heart.png | Bin 0 -> 808 bytes
src/main/resources/emoji/graphics/blush.png | Bin 0 -> 1025 bytes
src/main/resources/emoji/graphics/boar.png | Bin 0 -> 1330 bytes
src/main/resources/emoji/graphics/boat.png | Bin 0 -> 828 bytes
src/main/resources/emoji/graphics/bomb.png | Bin 0 -> 810 bytes
src/main/resources/emoji/graphics/book.png | Bin 0 -> 714 bytes
.../resources/emoji/graphics/bookmark.png | Bin 0 -> 738 bytes
.../emoji/graphics/bookmark_tabs.png | Bin 0 -> 689 bytes
src/main/resources/emoji/graphics/books.png | Bin 0 -> 749 bytes
src/main/resources/emoji/graphics/boom.png | Bin 0 -> 1197 bytes
src/main/resources/emoji/graphics/boot.png | Bin 0 -> 839 bytes
src/main/resources/emoji/graphics/bouquet.png | Bin 0 -> 1224 bytes
src/main/resources/emoji/graphics/bow.png | Bin 0 -> 1070 bytes
src/main/resources/emoji/graphics/bowling.png | Bin 0 -> 1021 bytes
src/main/resources/emoji/graphics/boy.png | Bin 0 -> 976 bytes
src/main/resources/emoji/graphics/bread.png | Bin 0 -> 731 bytes
.../emoji/graphics/bride_with_veil.png | Bin 0 -> 1223 bytes
.../emoji/graphics/bridge_at_night.png | Bin 0 -> 957 bytes
.../resources/emoji/graphics/briefcase.png | Bin 0 -> 681 bytes
.../resources/emoji/graphics/broken_heart.png | Bin 0 -> 1004 bytes
src/main/resources/emoji/graphics/bug.png | Bin 0 -> 1123 bytes
src/main/resources/emoji/graphics/bulb.png | Bin 0 -> 813 bytes
.../emoji/graphics/bullettrain_front.png | Bin 0 -> 645 bytes
.../emoji/graphics/bullettrain_side.png | Bin 0 -> 816 bytes
src/main/resources/emoji/graphics/bus.png | Bin 0 -> 760 bytes
src/main/resources/emoji/graphics/busstop.png | Bin 0 -> 507 bytes
.../emoji/graphics/bust_in_silhouette.png | Bin 0 -> 764 bytes
.../emoji/graphics/busts_in_silhouette.png | Bin 0 -> 815 bytes
src/main/resources/emoji/graphics/c.png | Bin 0 -> 760 bytes
src/main/resources/emoji/graphics/cactus.png | Bin 0 -> 861 bytes
src/main/resources/emoji/graphics/cake.png | Bin 0 -> 1301 bytes
.../resources/emoji/graphics/calendar.png | Bin 0 -> 993 bytes
src/main/resources/emoji/graphics/calling.png | Bin 0 -> 516 bytes
src/main/resources/emoji/graphics/camel.png | Bin 0 -> 1084 bytes
src/main/resources/emoji/graphics/camera.png | Bin 0 -> 898 bytes
src/main/resources/emoji/graphics/cancer.png | Bin 0 -> 1740 bytes
src/main/resources/emoji/graphics/candy.png | Bin 0 -> 1232 bytes
.../resources/emoji/graphics/capital_abcd.png | Bin 0 -> 1172 bytes
.../resources/emoji/graphics/capricorn.png | Bin 0 -> 887 bytes
src/main/resources/emoji/graphics/car.png | Bin 0 -> 795 bytes
.../resources/emoji/graphics/card_index.png | Bin 0 -> 631 bytes
.../emoji/graphics/carousel_horse.png | Bin 0 -> 1005 bytes
src/main/resources/emoji/graphics/cat.png | Bin 0 -> 1381 bytes
src/main/resources/emoji/graphics/cat2.png | Bin 0 -> 1020 bytes
src/main/resources/emoji/graphics/cd.png | Bin 0 -> 760 bytes
src/main/resources/emoji/graphics/chart.png | Bin 0 -> 996 bytes
.../graphics/chart_with_downwards_trend.png | Bin 0 -> 904 bytes
.../graphics/chart_with_upwards_trend.png | Bin 0 -> 901 bytes
.../emoji/graphics/checkered_flag.png | Bin 0 -> 696 bytes
.../resources/emoji/graphics/cherries.png | Bin 0 -> 940 bytes
.../emoji/graphics/cherry_blossom.png | Bin 0 -> 1382 bytes
.../resources/emoji/graphics/chestnut.png | Bin 0 -> 1412 bytes
src/main/resources/emoji/graphics/chicken.png | Bin 0 -> 954 bytes
.../emoji/graphics/children_crossing.png | Bin 0 -> 984 bytes
.../emoji/graphics/chocolate_bar.png | Bin 0 -> 869 bytes
.../emoji/graphics/christmas_tree.png | Bin 0 -> 1042 bytes
src/main/resources/emoji/graphics/church.png | Bin 0 -> 743 bytes
src/main/resources/emoji/graphics/cinema.png | Bin 0 -> 779 bytes
.../resources/emoji/graphics/circus_tent.png | Bin 0 -> 1049 bytes
.../resources/emoji/graphics/city_sunrise.png | Bin 0 -> 1052 bytes
.../resources/emoji/graphics/city_sunset.png | Bin 0 -> 599 bytes
src/main/resources/emoji/graphics/cl.png | Bin 0 -> 3493 bytes
src/main/resources/emoji/graphics/clap.png | Bin 0 -> 1151 bytes
src/main/resources/emoji/graphics/clapper.png | Bin 0 -> 771 bytes
.../resources/emoji/graphics/clipboard.png | Bin 0 -> 619 bytes
src/main/resources/emoji/graphics/clock1.png | Bin 0 -> 1065 bytes
src/main/resources/emoji/graphics/clock10.png | Bin 0 -> 1032 bytes
.../resources/emoji/graphics/clock1030.png | Bin 0 -> 1048 bytes
src/main/resources/emoji/graphics/clock11.png | Bin 0 -> 1060 bytes
.../resources/emoji/graphics/clock1130.png | Bin 0 -> 1078 bytes
src/main/resources/emoji/graphics/clock12.png | Bin 0 -> 1025 bytes
.../resources/emoji/graphics/clock1230.png | Bin 0 -> 956 bytes
.../resources/emoji/graphics/clock130.png | Bin 0 -> 1043 bytes
src/main/resources/emoji/graphics/clock2.png | Bin 0 -> 1057 bytes
.../resources/emoji/graphics/clock230.png | Bin 0 -> 1048 bytes
src/main/resources/emoji/graphics/clock3.png | Bin 0 -> 1009 bytes
.../resources/emoji/graphics/clock330.png | Bin 0 -> 1006 bytes
src/main/resources/emoji/graphics/clock4.png | Bin 0 -> 1055 bytes
.../resources/emoji/graphics/clock430.png | Bin 0 -> 1033 bytes
src/main/resources/emoji/graphics/clock5.png | Bin 0 -> 1041 bytes
.../resources/emoji/graphics/clock530.png | Bin 0 -> 1050 bytes
src/main/resources/emoji/graphics/clock6.png | Bin 0 -> 962 bytes
.../resources/emoji/graphics/clock630.png | Bin 0 -> 1009 bytes
src/main/resources/emoji/graphics/clock7.png | Bin 0 -> 1066 bytes
.../resources/emoji/graphics/clock730.png | Bin 0 -> 1063 bytes
src/main/resources/emoji/graphics/clock8.png | Bin 0 -> 1046 bytes
.../resources/emoji/graphics/clock830.png | Bin 0 -> 1040 bytes
src/main/resources/emoji/graphics/clock9.png | Bin 0 -> 1001 bytes
.../resources/emoji/graphics/clock930.png | Bin 0 -> 989 bytes
.../resources/emoji/graphics/closed_book.png | Bin 0 -> 645 bytes
.../emoji/graphics/closed_lock_with_key.png | Bin 0 -> 842 bytes
.../emoji/graphics/closed_umbrella.png | Bin 0 -> 951 bytes
src/main/resources/emoji/graphics/cloud.png | Bin 0 -> 784 bytes
src/main/resources/emoji/graphics/clubs.png | Bin 0 -> 838 bytes
src/main/resources/emoji/graphics/cn.png | Bin 0 -> 601 bytes
.../resources/emoji/graphics/cocktail.png | Bin 0 -> 918 bytes
src/main/resources/emoji/graphics/coffee.png | Bin 0 -> 1364 bytes
.../resources/emoji/graphics/cold_sweat.png | Bin 0 -> 1178 bytes
.../resources/emoji/graphics/collision.png | Bin 0 -> 1197 bytes
.../resources/emoji/graphics/computer.png | Bin 0 -> 642 bytes
.../emoji/graphics/confetti_ball.png | Bin 0 -> 1258 bytes
.../resources/emoji/graphics/confounded.png | Bin 0 -> 1217 bytes
.../resources/emoji/graphics/confused.png | Bin 0 -> 860 bytes
.../emoji/graphics/congratulations.png | Bin 0 -> 1244 bytes
.../resources/emoji/graphics/construction.png | Bin 0 -> 458 bytes
.../emoji/graphics/construction_worker.png | Bin 0 -> 1066 bytes
.../emoji/graphics/convenience_store.png | Bin 0 -> 763 bytes
src/main/resources/emoji/graphics/cookie.png | Bin 0 -> 1068 bytes
src/main/resources/emoji/graphics/cool.png | Bin 0 -> 940 bytes
src/main/resources/emoji/graphics/cop.png | Bin 0 -> 1172 bytes
.../resources/emoji/graphics/copyright.png | Bin 0 -> 1339 bytes
src/main/resources/emoji/graphics/corn.png | Bin 0 -> 1212 bytes
src/main/resources/emoji/graphics/couple.png | Bin 0 -> 1136 bytes
.../emoji/graphics/couple_with_heart.png | Bin 0 -> 985 bytes
.../resources/emoji/graphics/couplekiss.png | Bin 0 -> 1121 bytes
src/main/resources/emoji/graphics/cow.png | Bin 0 -> 990 bytes
src/main/resources/emoji/graphics/cow2.png | Bin 0 -> 1006 bytes
.../resources/emoji/graphics/credit_card.png | Bin 0 -> 624 bytes
.../emoji/graphics/crescent_moon.png | Bin 0 -> 903 bytes
.../resources/emoji/graphics/crocodile.png | Bin 0 -> 820 bytes
.../emoji/graphics/crossed_flags.png | Bin 0 -> 1055 bytes
src/main/resources/emoji/graphics/crown.png | Bin 0 -> 836 bytes
src/main/resources/emoji/graphics/cry.png | Bin 0 -> 1081 bytes
.../emoji/graphics/crying_cat_face.png | Bin 0 -> 1231 bytes
.../resources/emoji/graphics/crystal_ball.png | Bin 0 -> 862 bytes
src/main/resources/emoji/graphics/cupid.png | Bin 0 -> 1017 bytes
.../resources/emoji/graphics/curly_loop.png | Bin 0 -> 1015 bytes
.../emoji/graphics/currency_exchange.png | Bin 0 -> 1144 bytes
src/main/resources/emoji/graphics/curry.png | Bin 0 -> 1170 bytes
src/main/resources/emoji/graphics/custard.png | Bin 0 -> 1094 bytes
src/main/resources/emoji/graphics/customs.png | Bin 0 -> 803 bytes
src/main/resources/emoji/graphics/cyclone.png | Bin 0 -> 1330 bytes
src/main/resources/emoji/graphics/d.png | Bin 0 -> 689 bytes
src/main/resources/emoji/graphics/dancer.png | Bin 0 -> 1016 bytes
src/main/resources/emoji/graphics/dancers.png | Bin 0 -> 1157 bytes
src/main/resources/emoji/graphics/dango.png | Bin 0 -> 804 bytes
src/main/resources/emoji/graphics/dart.png | Bin 0 -> 1074 bytes
src/main/resources/emoji/graphics/dash.png | Bin 0 -> 918 bytes
src/main/resources/emoji/graphics/date.png | Bin 0 -> 1016 bytes
src/main/resources/emoji/graphics/de.png | Bin 0 -> 399 bytes
.../emoji/graphics/deciduous_tree.png | Bin 0 -> 876 bytes
.../emoji/graphics/department_store.png | Bin 0 -> 504 bytes
.../diamond_shape_with_a_dot_inside.png | Bin 0 -> 896 bytes
.../resources/emoji/graphics/diamonds.png | Bin 0 -> 645 bytes
.../resources/emoji/graphics/disappointed.png | Bin 0 -> 938 bytes
.../emoji/graphics/disappointed_relieved.png | Bin 0 -> 1088 bytes
src/main/resources/emoji/graphics/dizzy.png | Bin 0 -> 1079 bytes
.../resources/emoji/graphics/dizzy_face.png | Bin 0 -> 1088 bytes
.../emoji/graphics/do_not_litter.png | Bin 0 -> 1243 bytes
src/main/resources/emoji/graphics/dog.png | Bin 0 -> 1133 bytes
src/main/resources/emoji/graphics/dog2.png | Bin 0 -> 969 bytes
src/main/resources/emoji/graphics/doge.png | Bin 0 -> 3291 bytes
src/main/resources/emoji/graphics/dollar.png | Bin 0 -> 581 bytes
src/main/resources/emoji/graphics/dolls.png | Bin 0 -> 1394 bytes
src/main/resources/emoji/graphics/dolphin.png | Bin 0 -> 940 bytes
src/main/resources/emoji/graphics/door.png | Bin 0 -> 685 bytes
.../resources/emoji/graphics/doughnut.png | Bin 0 -> 1171 bytes
src/main/resources/emoji/graphics/dragon.png | Bin 0 -> 1474 bytes
.../resources/emoji/graphics/dragon_face.png | Bin 0 -> 1166 bytes
src/main/resources/emoji/graphics/dress.png | Bin 0 -> 1006 bytes
.../emoji/graphics/dromedary_camel.png | Bin 0 -> 1071 bytes
src/main/resources/emoji/graphics/droplet.png | Bin 0 -> 730 bytes
src/main/resources/emoji/graphics/dvd.png | Bin 0 -> 1132 bytes
src/main/resources/emoji/graphics/e-mail.png | Bin 0 -> 756 bytes
src/main/resources/emoji/graphics/e50a.png | Bin 0 -> 1131 bytes
src/main/resources/emoji/graphics/ear.png | Bin 0 -> 1190 bytes
.../resources/emoji/graphics/ear_of_rice.png | Bin 0 -> 1185 bytes
.../resources/emoji/graphics/earth_africa.png | Bin 0 -> 1243 bytes
.../emoji/graphics/earth_americas.png | Bin 0 -> 1177 bytes
.../resources/emoji/graphics/earth_asia.png | Bin 0 -> 1268 bytes
src/main/resources/emoji/graphics/egg.png | Bin 0 -> 1179 bytes
.../resources/emoji/graphics/eggplant.png | Bin 0 -> 821 bytes
src/main/resources/emoji/graphics/eight.png | Bin 0 -> 819 bytes
.../graphics/eight_pointed_black_star.png | Bin 0 -> 851 bytes
.../emoji/graphics/eight_spoked_asterisk.png | Bin 0 -> 755 bytes
.../emoji/graphics/electric_plug.png | Bin 0 -> 751 bytes
.../resources/emoji/graphics/elephant.png | Bin 0 -> 1107 bytes
src/main/resources/emoji/graphics/email.png | Bin 0 -> 715 bytes
src/main/resources/emoji/graphics/end.png | Bin 0 -> 1008 bytes
.../resources/emoji/graphics/envelope.png | Bin 0 -> 715 bytes
src/main/resources/emoji/graphics/es.png | Bin 0 -> 586 bytes
src/main/resources/emoji/graphics/euro.png | Bin 0 -> 564 bytes
.../emoji/graphics/european_castle.png | Bin 0 -> 648 bytes
.../emoji/graphics/european_post_office.png | Bin 0 -> 738 bytes
.../emoji/graphics/evergreen_tree.png | Bin 0 -> 1070 bytes
.../resources/emoji/graphics/exclamation.png | Bin 0 -> 266 bytes
.../emoji/graphics/expressionless.png | Bin 0 -> 801 bytes
.../resources/emoji/graphics/eyeglasses.png | Bin 0 -> 1057 bytes
src/main/resources/emoji/graphics/eyes.png | Bin 0 -> 706 bytes
src/main/resources/emoji/graphics/f.png | Bin 0 -> 574 bytes
.../resources/emoji/graphics/facepunch.png | Bin 0 -> 1040 bytes
src/main/resources/emoji/graphics/factory.png | Bin 0 -> 624 bytes
.../resources/emoji/graphics/fallen_leaf.png | Bin 0 -> 1486 bytes
src/main/resources/emoji/graphics/family.png | Bin 0 -> 1124 bytes
.../resources/emoji/graphics/fast_forward.png | Bin 0 -> 641 bytes
src/main/resources/emoji/graphics/fax.png | Bin 0 -> 858 bytes
src/main/resources/emoji/graphics/fearful.png | Bin 0 -> 1148 bytes
src/main/resources/emoji/graphics/feet.png | Bin 0 -> 803 bytes
.../resources/emoji/graphics/ferris_wheel.png | Bin 0 -> 1117 bytes
.../resources/emoji/graphics/file_folder.png | Bin 0 -> 610 bytes
src/main/resources/emoji/graphics/fire.png | Bin 0 -> 1118 bytes
.../resources/emoji/graphics/fire_engine.png | Bin 0 -> 732 bytes
.../resources/emoji/graphics/fireworks.png | Bin 0 -> 1285 bytes
.../emoji/graphics/first_quarter_moon.png | Bin 0 -> 1051 bytes
.../graphics/first_quarter_moon_with_face.png | Bin 0 -> 963 bytes
src/main/resources/emoji/graphics/fish.png | Bin 0 -> 1055 bytes
.../resources/emoji/graphics/fish_cake.png | Bin 0 -> 1319 bytes
.../emoji/graphics/fishing_pole_and_fish.png | Bin 0 -> 1221 bytes
src/main/resources/emoji/graphics/fist.png | Bin 0 -> 1007 bytes
src/main/resources/emoji/graphics/five.png | Bin 0 -> 768 bytes
src/main/resources/emoji/graphics/flags.png | Bin 0 -> 922 bytes
.../resources/emoji/graphics/flashlight.png | Bin 0 -> 679 bytes
.../resources/emoji/graphics/floppy_disk.png | Bin 0 -> 517 bytes
.../emoji/graphics/flower_playing_cards.png | Bin 0 -> 693 bytes
src/main/resources/emoji/graphics/flushed.png | Bin 0 -> 1129 bytes
src/main/resources/emoji/graphics/foggy.png | Bin 0 -> 810 bytes
.../resources/emoji/graphics/football.png | Bin 0 -> 1052 bytes
.../emoji/graphics/fork_and_knife.png | Bin 0 -> 677 bytes
.../resources/emoji/graphics/fountain.png | Bin 0 -> 1181 bytes
src/main/resources/emoji/graphics/four.png | Bin 0 -> 686 bytes
.../emoji/graphics/four_leaf_clover.png | Bin 0 -> 1089 bytes
src/main/resources/emoji/graphics/fr.png | Bin 0 -> 420 bytes
src/main/resources/emoji/graphics/free.png | Bin 0 -> 839 bytes
.../resources/emoji/graphics/fried_shrimp.png | Bin 0 -> 1079 bytes
src/main/resources/emoji/graphics/fries.png | Bin 0 -> 1192 bytes
src/main/resources/emoji/graphics/frog.png | Bin 0 -> 941 bytes
.../resources/emoji/graphics/frowning.png | Bin 0 -> 886 bytes
.../resources/emoji/graphics/fuelpump.png | Bin 0 -> 945 bytes
.../resources/emoji/graphics/full_moon.png | Bin 0 -> 1023 bytes
.../emoji/graphics/full_moon_with_face.png | Bin 0 -> 1112 bytes
src/main/resources/emoji/graphics/g.png | Bin 0 -> 883 bytes
.../resources/emoji/graphics/game_die.png | Bin 0 -> 1027 bytes
src/main/resources/emoji/graphics/gb.png | Bin 0 -> 881 bytes
src/main/resources/emoji/graphics/gem.png | Bin 0 -> 857 bytes
src/main/resources/emoji/graphics/gemini.png | Bin 0 -> 641 bytes
src/main/resources/emoji/graphics/ghost.png | Bin 0 -> 1109 bytes
src/main/resources/emoji/graphics/gift.png | Bin 0 -> 696 bytes
.../resources/emoji/graphics/gift_heart.png | Bin 0 -> 949 bytes
src/main/resources/emoji/graphics/girl.png | Bin 0 -> 1120 bytes
.../emoji/graphics/globe_with_meridians.png | Bin 0 -> 1235 bytes
src/main/resources/emoji/graphics/goat.png | Bin 0 -> 981 bytes
src/main/resources/emoji/graphics/golf.png | Bin 0 -> 828 bytes
src/main/resources/emoji/graphics/grapes.png | Bin 0 -> 996 bytes
.../resources/emoji/graphics/green_apple.png | Bin 0 -> 871 bytes
.../resources/emoji/graphics/green_book.png | Bin 0 -> 665 bytes
.../resources/emoji/graphics/green_heart.png | Bin 0 -> 810 bytes
.../emoji/graphics/grey_exclamation.png | Bin 0 -> 266 bytes
.../emoji/graphics/grey_question.png | Bin 0 -> 754 bytes
.../resources/emoji/graphics/grimacing.png | Bin 0 -> 882 bytes
src/main/resources/emoji/graphics/grin.png | Bin 0 -> 1120 bytes
.../resources/emoji/graphics/grinning.png | Bin 0 -> 1041 bytes
.../resources/emoji/graphics/guardsman.png | Bin 0 -> 916 bytes
src/main/resources/emoji/graphics/guitar.png | Bin 0 -> 830 bytes
src/main/resources/emoji/graphics/gun.png | Bin 0 -> 884 bytes
src/main/resources/emoji/graphics/haircut.png | Bin 0 -> 1151 bytes
.../resources/emoji/graphics/hamburger.png | Bin 0 -> 1099 bytes
src/main/resources/emoji/graphics/hammer.png | Bin 0 -> 619 bytes
src/main/resources/emoji/graphics/hamster.png | Bin 0 -> 1236 bytes
src/main/resources/emoji/graphics/hand.png | Bin 0 -> 775 bytes
src/main/resources/emoji/graphics/handbag.png | Bin 0 -> 906 bytes
src/main/resources/emoji/graphics/hankey.png | Bin 0 -> 1010 bytes
src/main/resources/emoji/graphics/hash.png | Bin 0 -> 837 bytes
.../emoji/graphics/hatched_chick.png | Bin 0 -> 968 bytes
.../emoji/graphics/hatching_chick.png | Bin 0 -> 1027 bytes
.../resources/emoji/graphics/headphones.png | Bin 0 -> 1003 bytes
.../resources/emoji/graphics/hear_no_evil.png | Bin 0 -> 1329 bytes
src/main/resources/emoji/graphics/heart.png | Bin 0 -> 808 bytes
.../emoji/graphics/heart_decoration.png | Bin 0 -> 795 bytes
.../resources/emoji/graphics/heart_eyes.png | Bin 0 -> 1070 bytes
.../emoji/graphics/heart_eyes_cat.png | Bin 0 -> 1316 bytes
.../resources/emoji/graphics/heartbeat.png | Bin 0 -> 1010 bytes
.../resources/emoji/graphics/heartpulse.png | Bin 0 -> 1208 bytes
src/main/resources/emoji/graphics/hearts.png | Bin 0 -> 791 bytes
.../emoji/graphics/heavy_check_mark.png | Bin 0 -> 428 bytes
.../emoji/graphics/heavy_division_sign.png | Bin 0 -> 441 bytes
.../emoji/graphics/heavy_dollar_sign.png | Bin 0 -> 870 bytes
.../emoji/graphics/heavy_exclamation_mark.png | Bin 0 -> 266 bytes
.../emoji/graphics/heavy_minus_sign.png | Bin 0 -> 247 bytes
.../emoji/graphics/heavy_multiplication_x.png | Bin 0 -> 681 bytes
.../emoji/graphics/heavy_plus_sign.png | Bin 0 -> 310 bytes
.../resources/emoji/graphics/helicopter.png | Bin 0 -> 1063 bytes
src/main/resources/emoji/graphics/herb.png | Bin 0 -> 1200 bytes
.../resources/emoji/graphics/hibiscus.png | Bin 0 -> 1224 bytes
.../emoji/graphics/high_brightness.png | Bin 0 -> 1046 bytes
.../resources/emoji/graphics/high_heel.png | Bin 0 -> 909 bytes
src/main/resources/emoji/graphics/hocho.png | Bin 0 -> 752 bytes
.../resources/emoji/graphics/honey_pot.png | Bin 0 -> 1107 bytes
.../resources/emoji/graphics/honeybee.png | Bin 0 -> 1064 bytes
src/main/resources/emoji/graphics/horse.png | Bin 0 -> 1028 bytes
.../resources/emoji/graphics/horse_racing.png | Bin 0 -> 1332 bytes
.../resources/emoji/graphics/hospital.png | Bin 0 -> 379 bytes
src/main/resources/emoji/graphics/hotel.png | Bin 0 -> 433 bytes
.../resources/emoji/graphics/hotsprings.png | Bin 0 -> 990 bytes
.../resources/emoji/graphics/hourglass.png | Bin 0 -> 869 bytes
.../emoji/graphics/hourglass_flowing_sand.png | Bin 0 -> 895 bytes
src/main/resources/emoji/graphics/house.png | Bin 0 -> 401 bytes
.../emoji/graphics/house_with_garden.png | Bin 0 -> 776 bytes
src/main/resources/emoji/graphics/huaji.gif | Bin 0 -> 13662 bytes
src/main/resources/emoji/graphics/hushed.png | Bin 0 -> 1011 bytes
src/main/resources/emoji/graphics/i.png | Bin 0 -> 505 bytes
.../resources/emoji/graphics/ice_cream.png | Bin 0 -> 961 bytes
.../resources/emoji/graphics/icecream.png | Bin 0 -> 1030 bytes
src/main/resources/emoji/graphics/id.png | Bin 0 -> 505 bytes
.../emoji/graphics/ideograph_advantage.png | Bin 0 -> 1492 bytes
src/main/resources/emoji/graphics/imp.png | Bin 0 -> 1018 bytes
.../resources/emoji/graphics/inbox_tray.png | Bin 0 -> 663 bytes
.../emoji/graphics/incoming_envelope.png | Bin 0 -> 892 bytes
.../graphics/information_desk_person.png | Bin 0 -> 892 bytes
.../emoji/graphics/information_source.png | Bin 0 -> 562 bytes
.../resources/emoji/graphics/innocent.png | Bin 0 -> 1137 bytes
.../resources/emoji/graphics/interrobang.png | Bin 0 -> 808 bytes
src/main/resources/emoji/graphics/iphone.png | Bin 0 -> 304 bytes
src/main/resources/emoji/graphics/it.png | Bin 0 -> 417 bytes
.../emoji/graphics/izakaya_lantern.png | Bin 0 -> 717 bytes
src/main/resources/emoji/graphics/j.png | Bin 0 -> 595 bytes
.../emoji/graphics/jack_o_lantern.png | Bin 0 -> 1133 bytes
src/main/resources/emoji/graphics/japan.png | Bin 0 -> 806 bytes
.../emoji/graphics/japanese_castle.png | Bin 0 -> 598 bytes
.../emoji/graphics/japanese_goblin.png | Bin 0 -> 1141 bytes
.../emoji/graphics/japanese_ogre.png | Bin 0 -> 1304 bytes
src/main/resources/emoji/graphics/jeans.png | Bin 0 -> 907 bytes
src/main/resources/emoji/graphics/joy.png | Bin 0 -> 1214 bytes
src/main/resources/emoji/graphics/joy_cat.png | Bin 0 -> 1396 bytes
src/main/resources/emoji/graphics/jp.png | Bin 0 -> 574 bytes
src/main/resources/emoji/graphics/k.png | Bin 0 -> 738 bytes
src/main/resources/emoji/graphics/key.png | Bin 0 -> 814 bytes
.../resources/emoji/graphics/keycap_ten.png | Bin 0 -> 937 bytes
src/main/resources/emoji/graphics/kimono.png | Bin 0 -> 1000 bytes
src/main/resources/emoji/graphics/kiss.png | Bin 0 -> 921 bytes
src/main/resources/emoji/graphics/kissing.png | Bin 0 -> 901 bytes
.../resources/emoji/graphics/kissing_cat.png | Bin 0 -> 1160 bytes
.../emoji/graphics/kissing_closed_eyes.png | Bin 0 -> 1146 bytes
.../emoji/graphics/kissing_heart.png | Bin 0 -> 1106 bytes
.../emoji/graphics/kissing_smiling_eyes.png | Bin 0 -> 926 bytes
src/main/resources/emoji/graphics/koala.png | Bin 0 -> 1024 bytes
src/main/resources/emoji/graphics/koko.png | Bin 0 -> 594 bytes
src/main/resources/emoji/graphics/kr.png | Bin 0 -> 918 bytes
.../emoji/graphics/large_blue_circle.png | Bin 0 -> 748 bytes
.../emoji/graphics/large_blue_diamond.png | Bin 0 -> 357 bytes
.../emoji/graphics/large_orange_diamond.png | Bin 0 -> 357 bytes
.../emoji/graphics/last_quarter_moon.png | Bin 0 -> 1030 bytes
.../graphics/last_quarter_moon_with_face.png | Bin 0 -> 966 bytes
.../resources/emoji/graphics/laughing.png | Bin 0 -> 1142 bytes
src/main/resources/emoji/graphics/leaves.png | Bin 0 -> 1365 bytes
src/main/resources/emoji/graphics/ledger.png | Bin 0 -> 802 bytes
.../resources/emoji/graphics/left_luggage.png | Bin 0 -> 737 bytes
.../emoji/graphics/left_right_arrow.png | Bin 0 -> 620 bytes
.../graphics/leftwards_arrow_with_hook.png | Bin 0 -> 738 bytes
src/main/resources/emoji/graphics/lemon.png | Bin 0 -> 905 bytes
src/main/resources/emoji/graphics/leo.png | Bin 0 -> 996 bytes
src/main/resources/emoji/graphics/leopard.png | Bin 0 -> 901 bytes
src/main/resources/emoji/graphics/libra.png | Bin 0 -> 825 bytes
.../resources/emoji/graphics/light_rail.png | Bin 0 -> 800 bytes
src/main/resources/emoji/graphics/link.png | Bin 0 -> 1000 bytes
src/main/resources/emoji/graphics/lips.png | Bin 0 -> 854 bytes
.../resources/emoji/graphics/lipstick.png | Bin 0 -> 670 bytes
src/main/resources/emoji/graphics/lock.png | Bin 0 -> 687 bytes
.../emoji/graphics/lock_with_ink_pen.png | Bin 0 -> 807 bytes
.../resources/emoji/graphics/lollipop.png | Bin 0 -> 1443 bytes
src/main/resources/emoji/graphics/loop.png | Bin 0 -> 1083 bytes
.../resources/emoji/graphics/loudspeaker.png | Bin 0 -> 786 bytes
.../resources/emoji/graphics/love_hotel.png | Bin 0 -> 660 bytes
.../resources/emoji/graphics/love_letter.png | Bin 0 -> 796 bytes
.../emoji/graphics/low_brightness.png | Bin 0 -> 916 bytes
src/main/resources/emoji/graphics/m.png | Bin 0 -> 1212 bytes
src/main/resources/emoji/graphics/mag.png | Bin 0 -> 873 bytes
.../resources/emoji/graphics/mag_right.png | Bin 0 -> 863 bytes
src/main/resources/emoji/graphics/mahjong.png | Bin 0 -> 843 bytes
src/main/resources/emoji/graphics/mailbox.png | Bin 0 -> 669 bytes
.../emoji/graphics/mailbox_closed.png | Bin 0 -> 660 bytes
.../emoji/graphics/mailbox_with_mail.png | Bin 0 -> 768 bytes
.../emoji/graphics/mailbox_with_no_mail.png | Bin 0 -> 622 bytes
src/main/resources/emoji/graphics/man.png | Bin 0 -> 1003 bytes
.../emoji/graphics/man_with_gua_pi_mao.png | Bin 0 -> 1006 bytes
.../emoji/graphics/man_with_turban.png | Bin 0 -> 1010 bytes
.../resources/emoji/graphics/mans_shoe.png | Bin 0 -> 738 bytes
.../resources/emoji/graphics/maple_leaf.png | Bin 0 -> 921 bytes
src/main/resources/emoji/graphics/mask.png | Bin 0 -> 1039 bytes
src/main/resources/emoji/graphics/massage.png | Bin 0 -> 1010 bytes
.../resources/emoji/graphics/meat_on_bone.png | Bin 0 -> 950 bytes
src/main/resources/emoji/graphics/mega.png | Bin 0 -> 756 bytes
src/main/resources/emoji/graphics/melon.png | Bin 0 -> 1227 bytes
src/main/resources/emoji/graphics/memo.png | Bin 0 -> 861 bytes
src/main/resources/emoji/graphics/mens.png | Bin 0 -> 682 bytes
src/main/resources/emoji/graphics/metro.png | Bin 0 -> 1059 bytes
.../resources/emoji/graphics/microphone.png | Bin 0 -> 886 bytes
.../resources/emoji/graphics/microscope.png | Bin 0 -> 879 bytes
.../resources/emoji/graphics/milky_way.png | Bin 0 -> 1239 bytes
src/main/resources/emoji/graphics/minibus.png | Bin 0 -> 811 bytes
.../resources/emoji/graphics/minidisc.png | Bin 0 -> 1067 bytes
.../emoji/graphics/mobile_phone_off.png | Bin 0 -> 859 bytes
.../emoji/graphics/money_with_wings.png | Bin 0 -> 1189 bytes
.../resources/emoji/graphics/moneybag.png | Bin 0 -> 997 bytes
src/main/resources/emoji/graphics/monkey.png | Bin 0 -> 1315 bytes
.../resources/emoji/graphics/monkey_face.png | Bin 0 -> 1111 bytes
.../resources/emoji/graphics/monorail.png | Bin 0 -> 677 bytes
.../resources/emoji/graphics/mortar_board.png | Bin 0 -> 961 bytes
.../resources/emoji/graphics/mount_fuji.png | Bin 0 -> 668 bytes
.../emoji/graphics/mountain_bicyclist.png | Bin 0 -> 1359 bytes
.../emoji/graphics/mountain_cableway.png | Bin 0 -> 794 bytes
.../emoji/graphics/mountain_railway.png | Bin 0 -> 764 bytes
src/main/resources/emoji/graphics/mouse.png | Bin 0 -> 1222 bytes
src/main/resources/emoji/graphics/mouse2.png | Bin 0 -> 797 bytes
.../resources/emoji/graphics/movie_camera.png | Bin 0 -> 927 bytes
src/main/resources/emoji/graphics/moyai.png | Bin 0 -> 1243 bytes
src/main/resources/emoji/graphics/muscle.png | Bin 0 -> 977 bytes
.../resources/emoji/graphics/mushroom.png | Bin 0 -> 1021 bytes
.../emoji/graphics/musical_keyboard.png | Bin 0 -> 308 bytes
.../resources/emoji/graphics/musical_note.png | Bin 0 -> 685 bytes
.../emoji/graphics/musical_score.png | Bin 0 -> 1020 bytes
src/main/resources/emoji/graphics/mute.png | Bin 0 -> 526 bytes
.../resources/emoji/graphics/nail_care.png | Bin 0 -> 783 bytes
.../resources/emoji/graphics/name_badge.png | Bin 0 -> 916 bytes
src/main/resources/emoji/graphics/necktie.png | Bin 0 -> 968 bytes
.../graphics/negative_squared_cross_mark.png | Bin 0 -> 699 bytes
.../resources/emoji/graphics/neutral_face.png | Bin 0 -> 827 bytes
src/main/resources/emoji/graphics/new.png | Bin 0 -> 965 bytes
.../resources/emoji/graphics/new_moon.png | Bin 0 -> 1033 bytes
.../emoji/graphics/new_moon_with_face.png | Bin 0 -> 1119 bytes
.../resources/emoji/graphics/newspaper.png | Bin 0 -> 675 bytes
src/main/resources/emoji/graphics/ng.png | Bin 0 -> 714 bytes
src/main/resources/emoji/graphics/nine.png | Bin 0 -> 781 bytes
src/main/resources/emoji/graphics/no_bell.png | Bin 0 -> 831 bytes
.../resources/emoji/graphics/no_bicycles.png | Bin 0 -> 1457 bytes
.../resources/emoji/graphics/no_entry.png | Bin 0 -> 798 bytes
.../emoji/graphics/no_entry_sign.png | Bin 0 -> 1064 bytes
src/main/resources/emoji/graphics/no_good.png | Bin 0 -> 1074 bytes
.../emoji/graphics/no_mobile_phones.png | Bin 0 -> 1191 bytes
.../resources/emoji/graphics/no_mouth.png | Bin 0 -> 813 bytes
.../emoji/graphics/no_pedestrians.png | Bin 0 -> 1271 bytes
.../resources/emoji/graphics/no_smoking.png | Bin 0 -> 1217 bytes
.../emoji/graphics/non-potable_water.png | Bin 0 -> 1200 bytes
src/main/resources/emoji/graphics/nose.png | Bin 0 -> 867 bytes
.../resources/emoji/graphics/notebook.png | Bin 0 -> 676 bytes
.../notebook_with_decorative_cover.png | Bin 0 -> 697 bytes
src/main/resources/emoji/graphics/notes.png | Bin 0 -> 650 bytes
.../resources/emoji/graphics/nut_and_bolt.png | Bin 0 -> 837 bytes
src/main/resources/emoji/graphics/o.png | Bin 0 -> 1003 bytes
src/main/resources/emoji/graphics/o2.png | Bin 0 -> 869 bytes
src/main/resources/emoji/graphics/ocean.png | Bin 0 -> 910 bytes
src/main/resources/emoji/graphics/octocat.png | Bin 0 -> 3738 bytes
src/main/resources/emoji/graphics/octopus.png | Bin 0 -> 970 bytes
src/main/resources/emoji/graphics/oden.png | Bin 0 -> 796 bytes
src/main/resources/emoji/graphics/office.png | Bin 0 -> 424 bytes
src/main/resources/emoji/graphics/ok.png | Bin 0 -> 995 bytes
src/main/resources/emoji/graphics/ok_hand.png | Bin 0 -> 991 bytes
.../resources/emoji/graphics/ok_woman.png | Bin 0 -> 1165 bytes
.../resources/emoji/graphics/older_man.png | Bin 0 -> 1004 bytes
.../resources/emoji/graphics/older_woman.png | Bin 0 -> 987 bytes
src/main/resources/emoji/graphics/on.png | Bin 0 -> 1178 bytes
.../emoji/graphics/oncoming_automobile.png | Bin 0 -> 1076 bytes
.../resources/emoji/graphics/oncoming_bus.png | Bin 0 -> 720 bytes
.../emoji/graphics/oncoming_police_car.png | Bin 0 -> 953 bytes
.../emoji/graphics/oncoming_taxi.png | Bin 0 -> 909 bytes
src/main/resources/emoji/graphics/one.png | Bin 0 -> 512 bytes
.../emoji/graphics/open_file_folder.png | Bin 0 -> 705 bytes
.../resources/emoji/graphics/open_hands.png | Bin 0 -> 1203 bytes
.../resources/emoji/graphics/open_mouth.png | Bin 0 -> 896 bytes
.../resources/emoji/graphics/ophiuchus.png | Bin 0 -> 873 bytes
.../resources/emoji/graphics/orange_book.png | Bin 0 -> 657 bytes
.../resources/emoji/graphics/outbox_tray.png | Bin 0 -> 659 bytes
src/main/resources/emoji/graphics/ox.png | Bin 0 -> 772 bytes
src/main/resources/emoji/graphics/package.png | Bin 0 -> 902 bytes
.../emoji/graphics/page_facing_up.png | Bin 0 -> 607 bytes
.../emoji/graphics/page_with_curl.png | Bin 0 -> 609 bytes
src/main/resources/emoji/graphics/pager.png | Bin 0 -> 800 bytes
.../resources/emoji/graphics/palm_tree.png | Bin 0 -> 1024 bytes
.../resources/emoji/graphics/panda_face.png | Bin 0 -> 1335 bytes
.../resources/emoji/graphics/paperclip.png | Bin 0 -> 910 bytes
src/main/resources/emoji/graphics/parking.png | Bin 0 -> 631 bytes
.../emoji/graphics/part_alternation_mark.png | Bin 0 -> 882 bytes
.../resources/emoji/graphics/partly_sunny.png | Bin 0 -> 880 bytes
.../emoji/graphics/passport_control.png | Bin 0 -> 878 bytes
.../resources/emoji/graphics/paw_prints.png | Bin 0 -> 803 bytes
src/main/resources/emoji/graphics/peach.png | Bin 0 -> 1002 bytes
src/main/resources/emoji/graphics/pear.png | Bin 0 -> 787 bytes
src/main/resources/emoji/graphics/pencil.png | Bin 0 -> 861 bytes
src/main/resources/emoji/graphics/pencil2.png | Bin 0 -> 708 bytes
src/main/resources/emoji/graphics/penguin.png | Bin 0 -> 1136 bytes
src/main/resources/emoji/graphics/pensive.png | Bin 0 -> 985 bytes
.../emoji/graphics/performing_arts.png | Bin 0 -> 1184 bytes
.../resources/emoji/graphics/persevere.png | Bin 0 -> 1192 bytes
.../emoji/graphics/person_frowning.png | Bin 0 -> 865 bytes
.../emoji/graphics/person_with_blond_hair.png | Bin 0 -> 992 bytes
.../graphics/person_with_pouting_face.png | Bin 0 -> 870 bytes
src/main/resources/emoji/graphics/phone.png | Bin 0 -> 1042 bytes
src/main/resources/emoji/graphics/pig.png | Bin 0 -> 1185 bytes
src/main/resources/emoji/graphics/pig2.png | Bin 0 -> 791 bytes
.../resources/emoji/graphics/pig_nose.png | Bin 0 -> 720 bytes
src/main/resources/emoji/graphics/pill.png | Bin 0 -> 759 bytes
.../resources/emoji/graphics/pineapple.png | Bin 0 -> 1172 bytes
src/main/resources/emoji/graphics/pisces.png | Bin 0 -> 850 bytes
src/main/resources/emoji/graphics/pizza.png | Bin 0 -> 986 bytes
.../resources/emoji/graphics/point_down.png | Bin 0 -> 754 bytes
.../resources/emoji/graphics/point_left.png | Bin 0 -> 715 bytes
.../resources/emoji/graphics/point_right.png | Bin 0 -> 714 bytes
.../resources/emoji/graphics/point_up.png | Bin 0 -> 897 bytes
.../resources/emoji/graphics/point_up_2.png | Bin 0 -> 758 bytes
.../resources/emoji/graphics/police_car.png | Bin 0 -> 829 bytes
src/main/resources/emoji/graphics/poodle.png | Bin 0 -> 1100 bytes
src/main/resources/emoji/graphics/poop.png | Bin 0 -> 1010 bytes
.../resources/emoji/graphics/postal_horn.png | Bin 0 -> 1052 bytes
src/main/resources/emoji/graphics/postbox.png | Bin 0 -> 673 bytes
.../emoji/graphics/potable_water.png | Bin 0 -> 625 bytes
src/main/resources/emoji/graphics/pouch.png | Bin 0 -> 796 bytes
.../resources/emoji/graphics/poultry_leg.png | Bin 0 -> 777 bytes
src/main/resources/emoji/graphics/pound.png | Bin 0 -> 570 bytes
.../resources/emoji/graphics/pouting_cat.png | Bin 0 -> 1154 bytes
src/main/resources/emoji/graphics/pray.png | Bin 0 -> 960 bytes
.../resources/emoji/graphics/princess.png | Bin 0 -> 1012 bytes
src/main/resources/emoji/graphics/punch.png | Bin 0 -> 1040 bytes
.../resources/emoji/graphics/purple_heart.png | Bin 0 -> 807 bytes
src/main/resources/emoji/graphics/purse.png | Bin 0 -> 976 bytes
src/main/resources/emoji/graphics/pushpin.png | Bin 0 -> 858 bytes
.../graphics/put_litter_in_its_place.png | Bin 0 -> 695 bytes
.../resources/emoji/graphics/question.png | Bin 0 -> 758 bytes
src/main/resources/emoji/graphics/r.png | Bin 0 -> 730 bytes
src/main/resources/emoji/graphics/rabbit.png | Bin 0 -> 1283 bytes
src/main/resources/emoji/graphics/rabbit2.png | Bin 0 -> 1038 bytes
.../resources/emoji/graphics/racehorse.png | Bin 0 -> 1161 bytes
src/main/resources/emoji/graphics/radio.png | Bin 0 -> 848 bytes
.../resources/emoji/graphics/radio_button.png | Bin 0 -> 946 bytes
src/main/resources/emoji/graphics/rage.png | Bin 0 -> 999 bytes
.../resources/emoji/graphics/railway_car.png | Bin 0 -> 484 bytes
src/main/resources/emoji/graphics/rainbow.png | Bin 0 -> 1137 bytes
.../resources/emoji/graphics/raised_hand.png | Bin 0 -> 775 bytes
.../resources/emoji/graphics/raised_hands.png | Bin 0 -> 1183 bytes
.../resources/emoji/graphics/raising_hand.png | Bin 0 -> 926 bytes
src/main/resources/emoji/graphics/ram.png | Bin 0 -> 977 bytes
src/main/resources/emoji/graphics/ramen.png | Bin 0 -> 1515 bytes
src/main/resources/emoji/graphics/rat.png | Bin 0 -> 863 bytes
src/main/resources/emoji/graphics/recycle.png | Bin 0 -> 1034 bytes
src/main/resources/emoji/graphics/red_car.png | Bin 0 -> 795 bytes
.../resources/emoji/graphics/red_circle.png | Bin 0 -> 752 bytes
.../resources/emoji/graphics/registered.png | Bin 0 -> 1331 bytes
src/main/resources/emoji/graphics/relaxed.png | Bin 0 -> 1111 bytes
.../resources/emoji/graphics/relieved.png | Bin 0 -> 1005 bytes
src/main/resources/emoji/graphics/repeat.png | Bin 0 -> 848 bytes
.../resources/emoji/graphics/repeat_one.png | Bin 0 -> 935 bytes
.../resources/emoji/graphics/restroom.png | Bin 0 -> 914 bytes
.../emoji/graphics/revolving_hearts.png | Bin 0 -> 1087 bytes
src/main/resources/emoji/graphics/rewind.png | Bin 0 -> 661 bytes
src/main/resources/emoji/graphics/ribbon.png | Bin 0 -> 1064 bytes
src/main/resources/emoji/graphics/rice.png | Bin 0 -> 1132 bytes
.../resources/emoji/graphics/rice_ball.png | Bin 0 -> 971 bytes
.../resources/emoji/graphics/rice_cracker.png | Bin 0 -> 966 bytes
.../resources/emoji/graphics/rice_scene.png | Bin 0 -> 1282 bytes
src/main/resources/emoji/graphics/ring.png | Bin 0 -> 885 bytes
src/main/resources/emoji/graphics/rocket.png | Bin 0 -> 1076 bytes
.../emoji/graphics/roller_coaster.png | Bin 0 -> 790 bytes
src/main/resources/emoji/graphics/rooster.png | Bin 0 -> 932 bytes
src/main/resources/emoji/graphics/rose.png | Bin 0 -> 894 bytes
.../emoji/graphics/rotating_light.png | Bin 0 -> 736 bytes
.../emoji/graphics/round_pushpin.png | Bin 0 -> 743 bytes
src/main/resources/emoji/graphics/rowboat.png | Bin 0 -> 734 bytes
src/main/resources/emoji/graphics/ru.png | Bin 0 -> 401 bytes
.../emoji/graphics/rugby_football.png | Bin 0 -> 1151 bytes
src/main/resources/emoji/graphics/running.png | Bin 0 -> 955 bytes
.../graphics/running_shirt_with_sash.png | Bin 0 -> 1060 bytes
src/main/resources/emoji/graphics/sa.png | Bin 0 -> 799 bytes
.../resources/emoji/graphics/sagittarius.png | Bin 0 -> 602 bytes
.../resources/emoji/graphics/sailboat.png | Bin 0 -> 828 bytes
src/main/resources/emoji/graphics/sake.png | Bin 0 -> 1049 bytes
src/main/resources/emoji/graphics/sandal.png | Bin 0 -> 733 bytes
src/main/resources/emoji/graphics/santa.png | Bin 0 -> 1030 bytes
.../resources/emoji/graphics/satellite.png | Bin 0 -> 1104 bytes
.../resources/emoji/graphics/satisfied.png | Bin 0 -> 1142 bytes
.../resources/emoji/graphics/saxophone.png | Bin 0 -> 901 bytes
src/main/resources/emoji/graphics/school.png | Bin 0 -> 699 bytes
.../emoji/graphics/school_satchel.png | Bin 0 -> 1118 bytes
.../resources/emoji/graphics/scissors.png | Bin 0 -> 1185 bytes
.../resources/emoji/graphics/scorpius.png | Bin 0 -> 724 bytes
src/main/resources/emoji/graphics/scream.png | Bin 0 -> 1255 bytes
.../resources/emoji/graphics/scream_cat.png | Bin 0 -> 1359 bytes
src/main/resources/emoji/graphics/scroll.png | Bin 0 -> 623 bytes
src/main/resources/emoji/graphics/seat.png | Bin 0 -> 637 bytes
src/main/resources/emoji/graphics/secret.png | Bin 0 -> 1462 bytes
.../resources/emoji/graphics/see_no_evil.png | Bin 0 -> 1447 bytes
.../resources/emoji/graphics/seedling.png | Bin 0 -> 777 bytes
src/main/resources/emoji/graphics/seven.png | Bin 0 -> 698 bytes
.../resources/emoji/graphics/shaved_ice.png | Bin 0 -> 970 bytes
src/main/resources/emoji/graphics/sheep.png | Bin 0 -> 919 bytes
src/main/resources/emoji/graphics/shell.png | Bin 0 -> 1166 bytes
src/main/resources/emoji/graphics/ship.png | Bin 0 -> 604 bytes
src/main/resources/emoji/graphics/shirt.png | Bin 0 -> 864 bytes
src/main/resources/emoji/graphics/shoe.png | Bin 0 -> 738 bytes
src/main/resources/emoji/graphics/shower.png | Bin 0 -> 802 bytes
.../emoji/graphics/signal_strength.png | Bin 0 -> 310 bytes
src/main/resources/emoji/graphics/six.png | Bin 0 -> 795 bytes
.../emoji/graphics/six_pointed_star.png | Bin 0 -> 1170 bytes
src/main/resources/emoji/graphics/ski.png | Bin 0 -> 1081 bytes
src/main/resources/emoji/graphics/skull.png | Bin 0 -> 919 bytes
.../resources/emoji/graphics/sleeping.png | Bin 0 -> 1026 bytes
src/main/resources/emoji/graphics/sleepy.png | Bin 0 -> 1084 bytes
.../resources/emoji/graphics/slot_machine.png | Bin 0 -> 718 bytes
.../emoji/graphics/small_blue_diamond.png | Bin 0 -> 302 bytes
.../emoji/graphics/small_orange_diamond.png | Bin 0 -> 302 bytes
.../emoji/graphics/small_red_triangle.png | Bin 0 -> 558 bytes
.../graphics/small_red_triangle_down.png | Bin 0 -> 559 bytes
src/main/resources/emoji/graphics/smile.png | Bin 0 -> 1034 bytes
.../resources/emoji/graphics/smile_cat.png | Bin 0 -> 1308 bytes
src/main/resources/emoji/graphics/smiley.png | Bin 0 -> 1059 bytes
.../resources/emoji/graphics/smiley_cat.png | Bin 0 -> 1183 bytes
.../resources/emoji/graphics/smiling_imp.png | Bin 0 -> 1097 bytes
src/main/resources/emoji/graphics/smirk.png | Bin 0 -> 1011 bytes
.../resources/emoji/graphics/smirk_cat.png | Bin 0 -> 1134 bytes
src/main/resources/emoji/graphics/smoking.png | Bin 0 -> 751 bytes
src/main/resources/emoji/graphics/snail.png | Bin 0 -> 1135 bytes
src/main/resources/emoji/graphics/snake.png | Bin 0 -> 1105 bytes
.../resources/emoji/graphics/snowboarder.png | Bin 0 -> 1172 bytes
.../resources/emoji/graphics/snowflake.png | Bin 0 -> 1116 bytes
src/main/resources/emoji/graphics/snowman.png | Bin 0 -> 929 bytes
src/main/resources/emoji/graphics/sob.png | Bin 0 -> 1149 bytes
src/main/resources/emoji/graphics/soccer.png | Bin 0 -> 1417 bytes
src/main/resources/emoji/graphics/soon.png | Bin 0 -> 1137 bytes
src/main/resources/emoji/graphics/sos.png | Bin 0 -> 1074 bytes
src/main/resources/emoji/graphics/sound.png | Bin 0 -> 652 bytes
.../emoji/graphics/space_invader.png | Bin 0 -> 156 bytes
src/main/resources/emoji/graphics/spades.png | Bin 0 -> 831 bytes
.../resources/emoji/graphics/spaghetti.png | Bin 0 -> 1516 bytes
src/main/resources/emoji/graphics/sparkle.png | Bin 0 -> 870 bytes
.../resources/emoji/graphics/sparkler.png | Bin 0 -> 1428 bytes
.../resources/emoji/graphics/sparkles.png | Bin 0 -> 952 bytes
.../emoji/graphics/sparkling_heart.png | Bin 0 -> 991 bytes
.../emoji/graphics/speak_no_evil.png | Bin 0 -> 1325 bytes
src/main/resources/emoji/graphics/speaker.png | Bin 0 -> 318 bytes
.../emoji/graphics/speech_balloon.png | Bin 0 -> 830 bytes
.../resources/emoji/graphics/speedboat.png | Bin 0 -> 768 bytes
.../resources/emoji/graphics/squirrel.png | Bin 0 -> 1248 bytes
src/main/resources/emoji/graphics/star.png | Bin 0 -> 795 bytes
src/main/resources/emoji/graphics/star2.png | Bin 0 -> 1084 bytes
src/main/resources/emoji/graphics/stars.png | Bin 0 -> 967 bytes
src/main/resources/emoji/graphics/station.png | Bin 0 -> 1121 bytes
.../emoji/graphics/statue_of_liberty.png | Bin 0 -> 1484 bytes
.../emoji/graphics/steam_locomotive.png | Bin 0 -> 846 bytes
src/main/resources/emoji/graphics/stew.png | Bin 0 -> 1121 bytes
.../emoji/graphics/straight_ruler.png | Bin 0 -> 794 bytes
.../resources/emoji/graphics/strawberry.png | Bin 0 -> 1119 bytes
.../emoji/graphics/stuck_out_tongue.png | Bin 0 -> 998 bytes
.../graphics/stuck_out_tongue_closed_eyes.png | Bin 0 -> 1109 bytes
.../graphics/stuck_out_tongue_winking_eye.png | Bin 0 -> 1083 bytes
.../emoji/graphics/sun_with_face.png | Bin 0 -> 1185 bytes
.../resources/emoji/graphics/sunflower.png | Bin 0 -> 1065 bytes
.../resources/emoji/graphics/sunglasses.png | Bin 0 -> 650 bytes
src/main/resources/emoji/graphics/sunny.png | Bin 0 -> 873 bytes
src/main/resources/emoji/graphics/sunrise.png | Bin 0 -> 982 bytes
.../emoji/graphics/sunrise_over_mountains.png | Bin 0 -> 1049 bytes
src/main/resources/emoji/graphics/surfer.png | Bin 0 -> 1198 bytes
src/main/resources/emoji/graphics/sushi.png | Bin 0 -> 1227 bytes
.../emoji/graphics/suspension_railway.png | Bin 0 -> 724 bytes
src/main/resources/emoji/graphics/sweat.png | Bin 0 -> 972 bytes
.../resources/emoji/graphics/sweat_drops.png | Bin 0 -> 1017 bytes
.../resources/emoji/graphics/sweat_smile.png | Bin 0 -> 1139 bytes
.../resources/emoji/graphics/sweet_potato.png | Bin 0 -> 962 bytes
src/main/resources/emoji/graphics/swimmer.png | Bin 0 -> 926 bytes
src/main/resources/emoji/graphics/symbols.png | Bin 0 -> 1117 bytes
src/main/resources/emoji/graphics/syringe.png | Bin 0 -> 810 bytes
src/main/resources/emoji/graphics/tada.png | Bin 0 -> 1345 bytes
.../emoji/graphics/tanabata_tree.png | Bin 0 -> 1374 bytes
.../resources/emoji/graphics/tangerine.png | Bin 0 -> 849 bytes
src/main/resources/emoji/graphics/taurus.png | Bin 0 -> 877 bytes
src/main/resources/emoji/graphics/taxi.png | Bin 0 -> 778 bytes
src/main/resources/emoji/graphics/tea.png | Bin 0 -> 1313 bytes
.../resources/emoji/graphics/telephone.png | Bin 0 -> 1042 bytes
.../emoji/graphics/telephone_receiver.png | Bin 0 -> 832 bytes
.../resources/emoji/graphics/telescope.png | Bin 0 -> 984 bytes
src/main/resources/emoji/graphics/tennis.png | Bin 0 -> 923 bytes
src/main/resources/emoji/graphics/tent.png | Bin 0 -> 1118 bytes
.../emoji/graphics/thought_balloon.png | Bin 0 -> 812 bytes
src/main/resources/emoji/graphics/three.png | Bin 0 -> 819 bytes
.../resources/emoji/graphics/thumbsdown.png | Bin 0 -> 1090 bytes
.../resources/emoji/graphics/thumbsup.png | Bin 0 -> 1098 bytes
src/main/resources/emoji/graphics/ticket.png | Bin 0 -> 917 bytes
src/main/resources/emoji/graphics/tiger.png | Bin 0 -> 1257 bytes
src/main/resources/emoji/graphics/tiger2.png | Bin 0 -> 1029 bytes
.../resources/emoji/graphics/tired_face.png | Bin 0 -> 1242 bytes
src/main/resources/emoji/graphics/tm.png | Bin 0 -> 540 bytes
src/main/resources/emoji/graphics/toilet.png | Bin 0 -> 688 bytes
.../resources/emoji/graphics/tokyo_tower.png | Bin 0 -> 855 bytes
src/main/resources/emoji/graphics/tomato.png | Bin 0 -> 948 bytes
src/main/resources/emoji/graphics/tongue.png | Bin 0 -> 738 bytes
src/main/resources/emoji/graphics/top.png | Bin 0 -> 1024 bytes
src/main/resources/emoji/graphics/tophat.png | Bin 0 -> 997 bytes
src/main/resources/emoji/graphics/tractor.png | Bin 0 -> 1172 bytes
.../emoji/graphics/traffic_light.png | Bin 0 -> 670 bytes
src/main/resources/emoji/graphics/train.png | Bin 0 -> 749 bytes
src/main/resources/emoji/graphics/train2.png | Bin 0 -> 1209 bytes
src/main/resources/emoji/graphics/tram.png | Bin 0 -> 1011 bytes
.../graphics/triangular_flag_on_post.png | Bin 0 -> 580 bytes
.../emoji/graphics/triangular_ruler.png | Bin 0 -> 559 bytes
src/main/resources/emoji/graphics/trident.png | Bin 0 -> 902 bytes
src/main/resources/emoji/graphics/triumph.png | Bin 0 -> 1252 bytes
.../resources/emoji/graphics/trolleybus.png | Bin 0 -> 779 bytes
.../resources/emoji/graphics/trollface.png | Bin 0 -> 4901 bytes
src/main/resources/emoji/graphics/trophy.png | Bin 0 -> 984 bytes
.../emoji/graphics/tropical_drink.png | Bin 0 -> 1045 bytes
.../emoji/graphics/tropical_fish.png | Bin 0 -> 1140 bytes
src/main/resources/emoji/graphics/truck.png | Bin 0 -> 694 bytes
src/main/resources/emoji/graphics/trumpet.png | Bin 0 -> 991 bytes
src/main/resources/emoji/graphics/tshirt.png | Bin 0 -> 864 bytes
src/main/resources/emoji/graphics/tulip.png | Bin 0 -> 1047 bytes
src/main/resources/emoji/graphics/turtle.png | Bin 0 -> 1014 bytes
src/main/resources/emoji/graphics/tv.png | Bin 0 -> 540 bytes
.../graphics/twisted_rightwards_arrows.png | Bin 0 -> 797 bytes
src/main/resources/emoji/graphics/two.png | Bin 0 -> 751 bytes
.../resources/emoji/graphics/two_hearts.png | Bin 0 -> 880 bytes
.../emoji/graphics/two_men_holding_hands.png | Bin 0 -> 1076 bytes
.../graphics/two_women_holding_hands.png | Bin 0 -> 1072 bytes
src/main/resources/emoji/graphics/u.png | Bin 0 -> 698 bytes
src/main/resources/emoji/graphics/u5272.png | Bin 0 -> 801 bytes
src/main/resources/emoji/graphics/u5408.png | Bin 0 -> 779 bytes
src/main/resources/emoji/graphics/u55b6.png | Bin 0 -> 827 bytes
src/main/resources/emoji/graphics/u6307.png | Bin 0 -> 881 bytes
src/main/resources/emoji/graphics/u6708.png | Bin 0 -> 705 bytes
src/main/resources/emoji/graphics/u6709.png | Bin 0 -> 731 bytes
src/main/resources/emoji/graphics/u6e80.png | Bin 0 -> 933 bytes
src/main/resources/emoji/graphics/u7121.png | Bin 0 -> 894 bytes
src/main/resources/emoji/graphics/u7533.png | Bin 0 -> 613 bytes
src/main/resources/emoji/graphics/u7981.png | Bin 0 -> 1013 bytes
src/main/resources/emoji/graphics/u7a7a.png | Bin 0 -> 787 bytes
.../resources/emoji/graphics/umbrella.png | Bin 0 -> 1116 bytes
.../resources/emoji/graphics/unamused.png | Bin 0 -> 1013 bytes
.../resources/emoji/graphics/underage.png | Bin 0 -> 1357 bytes
.../resources/emoji/graphics/unicorn_face.png | Bin 0 -> 23177 bytes
src/main/resources/emoji/graphics/unlock.png | Bin 0 -> 668 bytes
src/main/resources/emoji/graphics/up.png | Bin 0 -> 827 bytes
src/main/resources/emoji/graphics/us.png | Bin 0 -> 519 bytes
src/main/resources/emoji/graphics/v.png | Bin 0 -> 1015 bytes
.../emoji/graphics/vertical_traffic_light.png | Bin 0 -> 689 bytes
src/main/resources/emoji/graphics/vhs.png | Bin 0 -> 699 bytes
.../emoji/graphics/vibration_mode.png | Bin 0 -> 669 bytes
.../resources/emoji/graphics/video_camera.png | Bin 0 -> 684 bytes
.../resources/emoji/graphics/video_game.png | Bin 0 -> 1017 bytes
src/main/resources/emoji/graphics/violin.png | Bin 0 -> 951 bytes
src/main/resources/emoji/graphics/virgo.png | Bin 0 -> 864 bytes
src/main/resources/emoji/graphics/volcano.png | Bin 0 -> 1193 bytes
src/main/resources/emoji/graphics/vs.png | Bin 0 -> 1072 bytes
src/main/resources/emoji/graphics/walking.png | Bin 0 -> 873 bytes
.../emoji/graphics/waning_crescent_moon.png | Bin 0 -> 1064 bytes
.../emoji/graphics/waning_gibbous_moon.png | Bin 0 -> 1079 bytes
src/main/resources/emoji/graphics/warning.png | Bin 0 -> 841 bytes
src/main/resources/emoji/graphics/watch.png | Bin 0 -> 880 bytes
.../emoji/graphics/water_buffalo.png | Bin 0 -> 881 bytes
.../resources/emoji/graphics/watermelon.png | Bin 0 -> 974 bytes
src/main/resources/emoji/graphics/wave.png | Bin 0 -> 1454 bytes
.../resources/emoji/graphics/wavy_dash.png | Bin 0 -> 750 bytes
.../emoji/graphics/waxing_crescent_moon.png | Bin 0 -> 1095 bytes
.../emoji/graphics/waxing_gibbous_moon.png | Bin 0 -> 1067 bytes
src/main/resources/emoji/graphics/wc.png | Bin 0 -> 1055 bytes
src/main/resources/emoji/graphics/weary.png | Bin 0 -> 1226 bytes
src/main/resources/emoji/graphics/wedding.png | Bin 0 -> 891 bytes
src/main/resources/emoji/graphics/whale.png | Bin 0 -> 1022 bytes
src/main/resources/emoji/graphics/whale2.png | Bin 0 -> 917 bytes
.../resources/emoji/graphics/wheelchair.png | Bin 0 -> 1011 bytes
.../emoji/graphics/white_check_mark.png | Bin 0 -> 591 bytes
.../resources/emoji/graphics/white_circle.png | Bin 0 -> 748 bytes
.../resources/emoji/graphics/white_flower.png | Bin 0 -> 1540 bytes
.../emoji/graphics/white_large_square.png | Bin 0 -> 341 bytes
.../graphics/white_medium_small_square.png | Bin 0 -> 236 bytes
.../emoji/graphics/white_medium_square.png | Bin 0 -> 264 bytes
.../emoji/graphics/white_small_square.png | Bin 0 -> 167 bytes
.../emoji/graphics/white_square_button.png | Bin 0 -> 452 bytes
.../resources/emoji/graphics/wind_chime.png | Bin 0 -> 832 bytes
.../resources/emoji/graphics/wine_glass.png | Bin 0 -> 919 bytes
src/main/resources/emoji/graphics/wink.png | Bin 0 -> 1096 bytes
src/main/resources/emoji/graphics/wolf.png | Bin 0 -> 1154 bytes
src/main/resources/emoji/graphics/woman.png | Bin 0 -> 1035 bytes
.../emoji/graphics/womans_clothes.png | Bin 0 -> 950 bytes
.../resources/emoji/graphics/womans_hat.png | Bin 0 -> 855 bytes
src/main/resources/emoji/graphics/womens.png | Bin 0 -> 769 bytes
src/main/resources/emoji/graphics/worried.png | Bin 0 -> 1009 bytes
src/main/resources/emoji/graphics/wrench.png | Bin 0 -> 790 bytes
src/main/resources/emoji/graphics/x.png | Bin 0 -> 592 bytes
.../resources/emoji/graphics/yellow_heart.png | Bin 0 -> 808 bytes
src/main/resources/emoji/graphics/yen.png | Bin 0 -> 533 bytes
src/main/resources/emoji/graphics/yum.png | Bin 0 -> 1023 bytes
src/main/resources/emoji/graphics/zap.png | Bin 0 -> 753 bytes
src/main/resources/emoji/graphics/zero.png | Bin 0 -> 811 bytes
src/main/resources/emoji/graphics/zzz.png | Bin 0 -> 652 bytes
src/main/resources/emoji/index.html | 961 +++
src/main/resources/emoji/script.js | 177 +
src/main/resources/emoji/style.css | 156 +
src/main/resources/etc/header.txt | 15 +
src/main/resources/halt.html | 615 ++
src/main/resources/images/404/0.gif | Bin 0 -> 116548 bytes
src/main/resources/images/404/1.gif | Bin 0 -> 67746 bytes
src/main/resources/images/404/2.gif | Bin 0 -> 167175 bytes
src/main/resources/images/404/3.gif | Bin 0 -> 721910 bytes
src/main/resources/images/404/4.gif | Bin 0 -> 22360 bytes
src/main/resources/images/404/5.gif | Bin 0 -> 106014 bytes
src/main/resources/images/404/6.gif | Bin 0 -> 109184 bytes
src/main/resources/images/H-20.png | Bin 0 -> 1264 bytes
.../resources/images/activities/1A0001.png | Bin 0 -> 5049 bytes
src/main/resources/images/activities/char.png | Bin 0 -> 2525 bytes
src/main/resources/images/activities/chat.png | Bin 0 -> 4166 bytes
.../resources/images/activities/checkin.png | Bin 0 -> 5989 bytes
.../resources/images/activities/gobang.png | Bin 0 -> 12345 bytes
src/main/resources/images/activities/snak.png | Bin 0 -> 6519 bytes
.../resources/images/activities/yesterday.png | Bin 0 -> 6188 bytes
src/main/resources/images/alipay-donate.png | Bin 0 -> 7768 bytes
src/main/resources/images/blank.png | Bin 0 -> 930 bytes
src/main/resources/images/close.png | Bin 0 -> 1362 bytes
src/main/resources/images/code-bg.png | Bin 0 -> 343 bytes
src/main/resources/images/favicon.png | Bin 0 -> 1757 bytes
src/main/resources/images/faviconH.png | Bin 0 -> 2834 bytes
src/main/resources/images/hacpai.png | Bin 0 -> 3231 bytes
src/main/resources/images/holiday/book-bg.jpg | Bin 0 -> 34698 bytes
.../resources/images/holiday/mc-banner.jpg | Bin 0 -> 57201 bytes
src/main/resources/images/holiday/mc-bg.png | Bin 0 -> 380273 bytes
src/main/resources/images/holiday/ny-bg.jpg | Bin 0 -> 322645 bytes
src/main/resources/images/holiday/ny-logo.png | Bin 0 -> 27253 bytes
src/main/resources/images/index-bg.svg | 1 +
src/main/resources/images/kill-browser.png | Bin 0 -> 8183 bytes
src/main/resources/images/livephoto.png | Bin 0 -> 7870 bytes
src/main/resources/images/logo.png | Bin 0 -> 11689 bytes
src/main/resources/images/m-char.png | Bin 0 -> 21305 bytes
.../resources/images/mail/verify-banner1.png | Bin 0 -> 33220 bytes
.../resources/images/mail/weekly-banner1.png | Bin 0 -> 33775 bytes
.../resources/images/mail/weekly-banner2.png | Bin 0 -> 51729 bytes
.../resources/images/mail/weekly-banner3.png | Bin 0 -> 79565 bytes
src/main/resources/images/music.png | Bin 0 -> 5285 bytes
src/main/resources/images/robot_avatar.jpg | Bin 0 -> 19742 bytes
.../images/services/algolia128x40.png | Bin 0 -> 6751 bytes
src/main/resources/images/sym-logo300.png | Bin 0 -> 3776 bytes
src/main/resources/images/tags/sym.png | Bin 0 -> 3267 bytes
src/main/resources/images/user-thumbnail.png | Bin 0 -> 3528 bytes
src/main/resources/js/activity.js | 211 +
src/main/resources/js/activity.min.js | 1 +
src/main/resources/js/add-article.js | 608 ++
src/main/resources/js/add-article.min.js | 1 +
src/main/resources/js/article.js | 1784 +++++
src/main/resources/js/article.min.js | 1 +
src/main/resources/js/breezemoon.js | 160 +
src/main/resources/js/breezemoon.min.js | 1 +
src/main/resources/js/channel.js | 333 +
src/main/resources/js/channel.min.js | 1 +
src/main/resources/js/chat-room.js | 106 +
src/main/resources/js/chat-room.min.js | 1 +
src/main/resources/js/common.js | 1780 +++++
src/main/resources/js/common.min.js | 1 +
src/main/resources/js/eating-snake.js | 347 +
src/main/resources/js/eating-snake.min.js | 1 +
src/main/resources/js/gobang.js | 262 +
src/main/resources/js/gobang.min.js | 1 +
.../resources/js/lib/algolia/algolia.min.js | 10 +
.../js/lib/algolia/algoliasearch.min.js | 3 +
.../js/lib/algolia/autocomplete.jquery.min.js | 6 +
.../resources/js/lib/aplayer/APlayer.min.js | 3 +
.../js/lib/compress/article-libs.min.js | 6 +
.../resources/js/lib/compress/article.min.css | 1 +
.../resources/js/lib/compress/libs.min.js | 8 +
.../resources/js/lib/diff2html/diff.min.js | 415 ++
.../js/lib/diff2html/diff2html-ui.min.js | 1 +
.../js/lib/diff2html/diff2html.min.css | 1 +
.../js/lib/diff2html/diff2html.min.js | 1 +
.../js/lib/echarts-2.2.7/chart/line.js | 1 +
.../resources/js/lib/echarts-2.2.7/echarts.js | 20 +
.../jquery.fileupload-process.js | 175 +
.../jquery.fileupload-validate.js | 122 +
.../file-upload-9.10.1/jquery.fileupload.js | 1468 ++++
.../jquery.fileupload.min.js | 5 +
.../jquery.iframe-transport.js | 217 +
.../vendor/jquery.ui.widget.js | 563 ++
.../js/lib/jquery/isotope.pkgd.min.js | 12 +
.../js/lib/jquery/jquery-3.1.0.min.js | 4 +
.../js/lib/jquery/jquery.bowknot.min.js | 685 ++
.../resources/js/lib/jquery/jquery.hotkeys.js | 204 +
.../js/lib/jquery/jquery.linkify-1.0-min.js | 3 +
.../resources/js/lib/jquery/jquery.pjax.js | 399 ++
.../js/lib/jquery/jquery.qrcode.min.js | 28 +
src/main/resources/js/lib/livephotoskit.js | 66 +
src/main/resources/js/lib/md5.js | 174 +
.../resources/js/lib/nprogress/nprogress.css | 73 +
.../resources/js/lib/nprogress/nprogress.js | 475 ++
.../js/lib/reconnecting-websocket.min.js | 1 +
.../js/lib/sound-recorder/SoundRecorder.js | 673 ++
src/main/resources/js/lib/ua-parser.min.js | 1 +
src/main/resources/js/m-article.js | 1329 ++++
src/main/resources/js/m-article.min.js | 1 +
src/main/resources/js/settings.js | 811 +++
src/main/resources/js/settings.min.js | 1 +
src/main/resources/js/symbol-defs.js | 67 +
src/main/resources/js/symbol-defs.min.js | 1 +
src/main/resources/js/verify.js | 400 ++
src/main/resources/js/verify.min.js | 1 +
src/main/resources/lang_en_US.properties | 1052 +++
src/main/resources/lang_zh_CN.properties | 1052 +++
src/main/resources/latke.properties | 35 +
.../resources/lib/net/pusuo/patchca-0.5.0.jar | Bin 0 -> 47900 bytes
src/main/resources/local.properties | 39 +
src/main/resources/log4j.properties | 49 +
.../resources/mail_tpl/sym_verifycode.ftl | 197 +
src/main/resources/mail_tpl/sym_weekly.ftl | 263 +
src/main/resources/repository.json | 1887 +++++
src/main/resources/robots.txt | 12 +
src/main/resources/scss/_common.scss | 519 ++
src/main/resources/scss/_variables.scss | 70 +
src/main/resources/scss/base.scss | 1426 ++++
src/main/resources/scss/error.scss | 56 +
src/main/resources/scss/home.scss | 594 ++
src/main/resources/scss/index.scss | 1326 ++++
src/main/resources/scss/mobile-base.scss | 2411 +++++++
src/main/resources/scss/responsive.scss | 209 +
.../skins/classic/activity/1A0001.ftl | 86 +
.../skins/classic/activity/character.ftl | 77 +
.../skins/classic/activity/eating-snake.ftl | 96 +
.../skins/classic/activity/gobang.ftl | 75 +
src/main/resources/skins/classic/admin/ad.ftl | 55 +
.../skins/classic/admin/add-article.ftl | 75 +
.../skins/classic/admin/add-domain.ftl | 43 +
.../skins/classic/admin/add-reserved-word.ftl | 41 +
.../resources/skins/classic/admin/add-tag.ftl | 41 +
.../skins/classic/admin/add-user.ftl | 60 +
.../resources/skins/classic/admin/article.ftl | 308 +
.../skins/classic/admin/articles.ftl | 82 +
.../skins/classic/admin/auditlog.ftl | 40 +
.../skins/classic/admin/breezemoon.ftl | 112 +
.../skins/classic/admin/breezemoons.ftl | 52 +
.../resources/skins/classic/admin/comment.ftl | 131 +
.../skins/classic/admin/comments.ftl | 53 +
.../resources/skins/classic/admin/domain.ftl | 175 +
.../resources/skins/classic/admin/domains.ftl | 56 +
.../resources/skins/classic/admin/error.ftl | 39 +
.../resources/skins/classic/admin/index.ftl | 73 +
.../skins/classic/admin/invitecode.ftl | 83 +
.../skins/classic/admin/invitecodes.ftl | 53 +
.../skins/classic/admin/macro-admin.ftl | 193 +
.../resources/skins/classic/admin/misc.ftl | 78 +
.../resources/skins/classic/admin/reports.ftl | 61 +
.../skins/classic/admin/reserved-word.ftl | 76 +
.../skins/classic/admin/reserved-words.ftl | 43 +
.../skins/classic/admin/role-permissions.ftl | 65 +
.../resources/skins/classic/admin/roles.ftl | 42 +
.../resources/skins/classic/admin/tag.ftl | 140 +
.../resources/skins/classic/admin/tags.ftl | 74 +
.../resources/skins/classic/admin/user.ftl | 485 ++
.../resources/skins/classic/admin/users.ftl | 72 +
src/main/resources/skins/classic/article.ftl | 592 ++
.../resources/skins/classic/breezemoon.ftl | 139 +
.../resources/skins/classic/charge-point.ftl | 68 +
.../resources/skins/classic/chat-room.ftl | 113 +
src/main/resources/skins/classic/city.ftl | 125 +
.../skins/classic/common/comment.ftl | 118 +
.../skins/classic/common/domains.ftl | 43 +
.../skins/classic/common/list-item.ftl | 92 +
.../skins/classic/common/person-info.ftl | 76 +
.../skins/classic/common/ranking.ftl | 26 +
.../skins/classic/common/title-icon.ftl | 33 +
.../skins/classic/domain-articles.ftl | 112 +
src/main/resources/skins/classic/domains.ftl | 77 +
.../resources/skins/classic/error/401.ftl | 48 +
.../resources/skins/classic/error/403.ftl | 48 +
.../resources/skins/classic/error/404.ftl | 45 +
.../resources/skins/classic/error/500.ftl | 41 +
.../resources/skins/classic/error/custom.ftl | 40 +
src/main/resources/skins/classic/footer.ftl | 123 +
src/main/resources/skins/classic/forward.ftl | 647 ++
src/main/resources/skins/classic/header.ftl | 114 +
.../skins/classic/home/activities.ftl | 144 +
.../skins/classic/home/breezemoons.ftl | 114 +
.../resources/skins/classic/home/comments.ftl | 73 +
.../skins/classic/home/followers.ftl | 87 +
.../skins/classic/home/following-articles.ftl | 73 +
.../skins/classic/home/following-tags.ftl | 81 +
.../skins/classic/home/following-users.ftl | 89 +
.../skins/classic/home/home-side.ftl | 138 +
.../resources/skins/classic/home/home.ftl | 73 +
.../skins/classic/home/macro-home.ftl | 142 +
.../skins/classic/home/notifications/at.ftl | 91 +
.../classic/home/notifications/broadcast.ftl | 58 +
.../classic/home/notifications/commented.ftl | 55 +
.../classic/home/notifications/following.ftl | 76 +
.../notifications/macro-notifications.ftl | 182 +
.../classic/home/notifications/point.ftl | 35 +
.../classic/home/notifications/reply.ftl | 55 +
.../home/notifications/sys-announce.ftl | 38 +
.../resources/skins/classic/home/points.ftl | 45 +
.../resources/skins/classic/home/post.ftl | 218 +
.../resources/skins/classic/home/pre-post.ftl | 86 +
.../skins/classic/home/settings/account.ftl | 94 +
.../skins/classic/home/settings/avatar.ftl | 62 +
.../skins/classic/home/settings/data.ftl | 30 +
.../skins/classic/home/settings/function.ftl | 116 +
.../skins/classic/home/settings/help.ftl | 48 +
.../skins/classic/home/settings/i18n.ftl | 48 +
.../skins/classic/home/settings/invite.ftl | 74 +
.../classic/home/settings/macro-settings.ftl | 82 +
.../skins/classic/home/settings/point.ftl | 38 +
.../skins/classic/home/settings/privacy.ftl | 139 +
.../skins/classic/home/settings/profile.ftl | 46 +
.../skins/classic/home/watching-articles.ftl | 73 +
src/main/resources/skins/classic/hot.ftl | 56 +
src/main/resources/skins/classic/index.ftl | 233 +
.../resources/skins/classic/macro-head.ftl | 35 +
.../resources/skins/classic/macro-list.ftl | 41 +
.../skins/classic/macro-pagination-query.ftl | 38 +
.../skins/classic/macro-pagination.ftl | 38 +
.../skins/classic/other/kill-browser.ftl | 24 +
src/main/resources/skins/classic/perfect.ftl | 52 +
src/main/resources/skins/classic/qna.ftl | 103 +
src/main/resources/skins/classic/recent.ftl | 102 +
.../skins/classic/search-articles.ftl | 54 +
src/main/resources/skins/classic/side.ftl | 167 +
.../resources/skins/classic/skin.properties | 27 +
.../resources/skins/classic/statistic.ftl | 234 +
.../resources/skins/classic/tag-articles.ftl | 174 +
src/main/resources/skins/classic/tags.ftl | 91 +
.../resources/skins/classic/top/balance.ftl | 67 +
.../resources/skins/classic/top/checkin.ftl | 63 +
.../skins/classic/top/consumption.ftl | 66 +
src/main/resources/skins/classic/top/link.ftl | 45 +
.../resources/skins/classic/top/macro-top.ftl | 69 +
.../skins/classic/verify/forget-pwd.ftl | 64 +
.../resources/skins/classic/verify/guide.ftl | 180 +
.../resources/skins/classic/verify/login.ftl | 77 +
.../skins/classic/verify/register.ftl | 82 +
.../skins/classic/verify/register2.ftl | 78 +
.../skins/classic/verify/reset-pwd.ftl | 71 +
src/main/resources/skins/classic/watch.ftl | 88 +
.../skins/mobile/activity/1A0001.ftl | 81 +
.../skins/mobile/activity/character.ftl | 69 +
src/main/resources/skins/mobile/admin/ad.ftl | 49 +
.../skins/mobile/admin/add-article.ftl | 51 +
.../skins/mobile/admin/add-domain.ftl | 40 +
.../skins/mobile/admin/add-reserved-word.ftl | 39 +
.../resources/skins/mobile/admin/add-tag.ftl | 38 +
.../resources/skins/mobile/admin/add-user.ftl | 48 +
.../resources/skins/mobile/admin/article.ftl | 217 +
.../resources/skins/mobile/admin/articles.ftl | 81 +
.../resources/skins/mobile/admin/auditlog.ftl | 43 +
.../skins/mobile/admin/breezemoon.ftl | 85 +
.../skins/mobile/admin/breezemoons.ftl | 47 +
.../resources/skins/mobile/admin/comment.ftl | 100 +
.../resources/skins/mobile/admin/comments.ftl | 48 +
.../resources/skins/mobile/admin/domain.ftl | 137 +
.../resources/skins/mobile/admin/domains.ftl | 56 +
.../resources/skins/mobile/admin/error.ftl | 40 +
.../resources/skins/mobile/admin/index.ftl | 68 +
.../skins/mobile/admin/invitecode.ftl | 65 +
.../skins/mobile/admin/invitecodes.ftl | 53 +
.../skins/mobile/admin/macro-admin.ftl | 223 +
.../resources/skins/mobile/admin/misc.ftl | 69 +
.../resources/skins/mobile/admin/reports.ftl | 61 +
.../skins/mobile/admin/reserved-word.ftl | 67 +
.../skins/mobile/admin/reserved-words.ftl | 43 +
.../skins/mobile/admin/role-permissions.ftl | 62 +
.../resources/skins/mobile/admin/roles.ftl | 42 +
src/main/resources/skins/mobile/admin/tag.ftl | 100 +
.../resources/skins/mobile/admin/tags.ftl | 57 +
.../resources/skins/mobile/admin/user.ftl | 355 +
.../resources/skins/mobile/admin/users.ftl | 66 +
src/main/resources/skins/mobile/article.ftl | 512 ++
.../resources/skins/mobile/breezemoon.ftl | 115 +
.../resources/skins/mobile/charge-point.ftl | 59 +
src/main/resources/skins/mobile/chat-room.ftl | 109 +
src/main/resources/skins/mobile/city.ftl | 60 +
.../resources/skins/mobile/common/comment.ftl | 118 +
.../skins/mobile/common/person-info.ftl | 61 +
.../resources/skins/mobile/common/ranking.ftl | 25 +
.../resources/skins/mobile/common/sub-nav.ftl | 35 +
.../skins/mobile/common/title-icon.ftl | 33 +
.../skins/mobile/domain-articles.ftl | 66 +
src/main/resources/skins/mobile/domains.ftl | 76 +
src/main/resources/skins/mobile/error/401.ftl | 56 +
src/main/resources/skins/mobile/error/403.ftl | 56 +
src/main/resources/skins/mobile/error/404.ftl | 40 +
src/main/resources/skins/mobile/error/500.ftl | 40 +
.../resources/skins/mobile/error/custom.ftl | 38 +
src/main/resources/skins/mobile/footer.ftl | 99 +
src/main/resources/skins/mobile/forward.ftl | 654 ++
src/main/resources/skins/mobile/header.ftl | 52 +
.../skins/mobile/home/activities.ftl | 114 +
.../skins/mobile/home/breezemoons.ftl | 88 +
.../resources/skins/mobile/home/comments.ftl | 59 +
.../resources/skins/mobile/home/followers.ftl | 69 +
.../skins/mobile/home/following-articles.ftl | 62 +
.../skins/mobile/home/following-tags.ftl | 64 +
.../skins/mobile/home/following-users.ftl | 69 +
.../resources/skins/mobile/home/home-side.ftl | 141 +
src/main/resources/skins/mobile/home/home.ftl | 61 +
.../skins/mobile/home/macro-home.ftl | 186 +
.../skins/mobile/home/notifications/at.ftl | 94 +
.../mobile/home/notifications/broadcast.ftl | 59 +
.../mobile/home/notifications/commented.ftl | 56 +
.../mobile/home/notifications/following.ftl | 78 +
.../notifications/macro-notifications.ftl | 162 +
.../skins/mobile/home/notifications/point.ftl | 35 +
.../skins/mobile/home/notifications/reply.ftl | 56 +
.../home/notifications/sys-announce.ftl | 35 +
.../resources/skins/mobile/home/points.ftl | 43 +
src/main/resources/skins/mobile/home/post.ftl | 215 +
.../resources/skins/mobile/home/pre-post.ftl | 69 +
.../skins/mobile/home/settings/account.ftl | 94 +
.../skins/mobile/home/settings/avatar.ftl | 63 +
.../skins/mobile/home/settings/data.ftl | 31 +
.../skins/mobile/home/settings/function.ftl | 91 +
.../skins/mobile/home/settings/help.ftl | 48 +
.../skins/mobile/home/settings/i18n.ftl | 49 +
.../skins/mobile/home/settings/invite.ftl | 62 +
.../mobile/home/settings/macro-settings.ftl | 100 +
.../skins/mobile/home/settings/point.ftl | 38 +
.../skins/mobile/home/settings/privacy.ftl | 138 +
.../skins/mobile/home/settings/profile.ftl | 49 +
.../skins/mobile/home/watching-articles.ftl | 62 +
src/main/resources/skins/mobile/hot.ftl | 45 +
src/main/resources/skins/mobile/index.ftl | 134 +
.../resources/skins/mobile/macro-head.ftl | 33 +
.../resources/skins/mobile/macro-list.ftl | 73 +
.../skins/mobile/macro-pagination-query.ftl | 42 +
.../skins/mobile/macro-pagination.ftl | 42 +
.../skins/mobile/other/kill-browser.ftl | 24 +
src/main/resources/skins/mobile/perfect.ftl | 72 +
src/main/resources/skins/mobile/qna.ftl | 74 +
src/main/resources/skins/mobile/recent.ftl | 73 +
src/main/resources/skins/mobile/side.ftl | 143 +
.../resources/skins/mobile/skin.properties | 27 +
src/main/resources/skins/mobile/statistic.ftl | 203 +
.../resources/skins/mobile/tag-articles.ftl | 138 +
src/main/resources/skins/mobile/tags.ftl | 106 +
.../resources/skins/mobile/top/balance.ftl | 69 +
.../resources/skins/mobile/top/checkin.ftl | 64 +
.../skins/mobile/top/consumption.ftl | 68 +
src/main/resources/skins/mobile/top/link.ftl | 45 +
.../resources/skins/mobile/top/macro-top.ftl | 57 +
.../skins/mobile/verify/forget-pwd.ftl | 62 +
.../resources/skins/mobile/verify/guide.ftl | 166 +
.../resources/skins/mobile/verify/login.ftl | 76 +
.../skins/mobile/verify/register.ftl | 81 +
.../skins/mobile/verify/register2.ftl | 77 +
.../skins/mobile/verify/reset-pwd.ftl | 70 +
src/main/resources/skins/mobile/watch.ftl | 69 +
src/main/resources/static-resources.xml | 109 +
src/main/resources/symphony.properties | 297 +
.../org/b3log/symphony/RepositoryJSONGen.java | 37 +
.../symphony/util/ElasticsearchTestCase.java | 67 +
.../b3log/symphony/util/PanguTestCase.java | 38 +
.../b3log/symphony/util/RedditScoreTest.java | 118 +
.../org/b3log/symphony/util/XSSTestCase.java | 50 +
src/test/resources/latke.properties | 35 +
src/test/resources/log4j.properties | 45 +
src/test/resources/markdown_syntax.text | 896 +++
1475 files changed, 117060 insertions(+), 2 deletions(-)
create mode 100644 .travis.yml
create mode 100644 CHANGE_LOGS.html
create mode 100644 Dockerfile
create mode 100644 LICENSE
create mode 100644 THIRD_PARTY_LICENSE
create mode 100644 gulpfile.js
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 pom.xml
create mode 100644 src/assembly/bin.xml
create mode 100644 src/main/java/org/b3log/symphony/Server.java
create mode 100644 src/main/java/org/b3log/symphony/cache/ArticleCache.java
create mode 100644 src/main/java/org/b3log/symphony/cache/CommentCache.java
create mode 100644 src/main/java/org/b3log/symphony/cache/DomainCache.java
create mode 100644 src/main/java/org/b3log/symphony/cache/OptionCache.java
create mode 100644 src/main/java/org/b3log/symphony/cache/TagCache.java
create mode 100644 src/main/java/org/b3log/symphony/cache/UserCache.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleAddAudioHandler.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleAddNotifier.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleBaiduSender.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleSearchAdder.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleSearchUpdater.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleUpdateAudioHandler.java
create mode 100644 src/main/java/org/b3log/symphony/event/ArticleUpdateNotifier.java
create mode 100644 src/main/java/org/b3log/symphony/event/CommentNotifier.java
create mode 100644 src/main/java/org/b3log/symphony/event/CommentUpdateNotifier.java
create mode 100644 src/main/java/org/b3log/symphony/event/EventTypes.java
create mode 100644 src/main/java/org/b3log/symphony/model/Article.java
create mode 100644 src/main/java/org/b3log/symphony/model/Breezemoon.java
create mode 100644 src/main/java/org/b3log/symphony/model/Character.java
create mode 100644 src/main/java/org/b3log/symphony/model/Comment.java
create mode 100644 src/main/java/org/b3log/symphony/model/Common.java
create mode 100644 src/main/java/org/b3log/symphony/model/Domain.java
create mode 100644 src/main/java/org/b3log/symphony/model/Emotion.java
create mode 100644 src/main/java/org/b3log/symphony/model/Follow.java
create mode 100644 src/main/java/org/b3log/symphony/model/Invitecode.java
create mode 100644 src/main/java/org/b3log/symphony/model/Link.java
create mode 100644 src/main/java/org/b3log/symphony/model/Liveness.java
create mode 100644 src/main/java/org/b3log/symphony/model/Notification.java
create mode 100644 src/main/java/org/b3log/symphony/model/Operation.java
create mode 100644 src/main/java/org/b3log/symphony/model/Option.java
create mode 100644 src/main/java/org/b3log/symphony/model/Permission.java
create mode 100644 src/main/java/org/b3log/symphony/model/Pointtransfer.java
create mode 100644 src/main/java/org/b3log/symphony/model/Referral.java
create mode 100644 src/main/java/org/b3log/symphony/model/Report.java
create mode 100644 src/main/java/org/b3log/symphony/model/Revision.java
create mode 100644 src/main/java/org/b3log/symphony/model/Reward.java
create mode 100644 src/main/java/org/b3log/symphony/model/Role.java
create mode 100644 src/main/java/org/b3log/symphony/model/Tag.java
create mode 100644 src/main/java/org/b3log/symphony/model/UserExt.java
create mode 100644 src/main/java/org/b3log/symphony/model/Verifycode.java
create mode 100644 src/main/java/org/b3log/symphony/model/Visit.java
create mode 100644 src/main/java/org/b3log/symphony/model/Vote.java
create mode 100644 src/main/java/org/b3log/symphony/model/feed/RSSCategory.java
create mode 100644 src/main/java/org/b3log/symphony/model/feed/RSSChannel.java
create mode 100644 src/main/java/org/b3log/symphony/model/feed/RSSItem.java
create mode 100644 src/main/java/org/b3log/symphony/model/sitemap/Sitemap.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ActivityProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/AdminProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/AfterRequestHandler.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ArticleProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/BeforeRequestHandler.java
create mode 100644 src/main/java/org/b3log/symphony/processor/BreezemoonProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/CaptchaProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ChargeProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ChatroomProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/CityProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/CommentProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/DomainProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ErrorProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/FeedProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/FetchUploadProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/FileUploadProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/FollowProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ForwardProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/IndexProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/LoginProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/NotificationProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/ReportProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/SearchProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/SettingsProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/SitemapProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/SkinRenderer.java
create mode 100644 src/main/java/org/b3log/symphony/processor/StatisticProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/TagProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/TopProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/UserProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/VoteProcessor.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/AnonymousViewCheck.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/CSRFCheck.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/CSRFToken.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/LoginCheck.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/PermissionCheck.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/PermissionGrant.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/UserBlockCheck.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchEndAdvice.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchStartAdvice.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001CollectValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001Validation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/ArticleAddValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/ArticleUpdateValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/ChatMsgAddValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/CommentAddValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/CommentUpdateValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/PointTransferValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/ShowArticleUpdateValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/UpdatePasswordValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/UpdateProfilesValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/UserForgetPwdValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/UserRegister2Validation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/advice/validate/UserRegisterValidation.java
create mode 100644 src/main/java/org/b3log/symphony/processor/channel/ArticleChannel.java
create mode 100644 src/main/java/org/b3log/symphony/processor/channel/ArticleListChannel.java
create mode 100644 src/main/java/org/b3log/symphony/processor/channel/ChatroomChannel.java
create mode 100644 src/main/java/org/b3log/symphony/processor/channel/GobangChannel.java
create mode 100644 src/main/java/org/b3log/symphony/processor/channel/UserChannel.java
create mode 100644 src/main/java/org/b3log/symphony/repository/ArticleRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/BreezemoonRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/CharacterRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/CommentRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/DomainRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/DomainTagRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/EmotionRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/FollowRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/InvitecodeRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/LinkRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/LivenessRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/NotificationRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/OperationRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/OptionRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/PermissionRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/PointtransferRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/ReferralRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/ReportRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/RevisionRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/RewardRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/RolePermissionRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/RoleRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/TagArticleRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/TagRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/TagTagRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/UserRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/UserRoleRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/UserTagRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/VerifycodeRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/VisitRepository.java
create mode 100644 src/main/java/org/b3log/symphony/repository/VoteRepository.java
create mode 100644 src/main/java/org/b3log/symphony/service/ActivityMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ActivityQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ArticleMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ArticleQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/AudioMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/AvatarQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/BreezemoonMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/BreezemoonQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/CacheMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/CharacterQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/CommentMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/CommentQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/CronMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/DataModelService.java
create mode 100644 src/main/java/org/b3log/symphony/service/DomainMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/DomainQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/EmotionMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/EmotionQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/FollowMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/FollowQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/InitMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/InvitecodeMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/InvitecodeQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/LinkMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/LinkQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/LivenessMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/LivenessQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/MailMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/NotificationMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/NotificationQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/OperationMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/OperationQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/OptionMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/OptionQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/PointtransferMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/PointtransferQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/PostExportService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ReferralMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ReportMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ReportQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/RevisionQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/RewardMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/RewardQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/RoleMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/RoleQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/SearchMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/SearchQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/ShortLinkQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/SitemapQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/TagMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/TagQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/UserMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/UserQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/VerifycodeMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/VerifycodeQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/service/VisitMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/VoteMgmtService.java
create mode 100644 src/main/java/org/b3log/symphony/service/VoteQueryService.java
create mode 100644 src/main/java/org/b3log/symphony/util/Emotions.java
create mode 100644 src/main/java/org/b3log/symphony/util/Escapes.java
create mode 100644 src/main/java/org/b3log/symphony/util/Geos.java
create mode 100644 src/main/java/org/b3log/symphony/util/Gravatars.java
create mode 100644 src/main/java/org/b3log/symphony/util/Headers.java
create mode 100644 src/main/java/org/b3log/symphony/util/Images.java
create mode 100644 src/main/java/org/b3log/symphony/util/JSONs.java
create mode 100644 src/main/java/org/b3log/symphony/util/Languages.java
create mode 100644 src/main/java/org/b3log/symphony/util/Links.java
create mode 100644 src/main/java/org/b3log/symphony/util/MP3Players.java
create mode 100644 src/main/java/org/b3log/symphony/util/Mails.java
create mode 100644 src/main/java/org/b3log/symphony/util/Markdowns.java
create mode 100644 src/main/java/org/b3log/symphony/util/Networks.java
create mode 100644 src/main/java/org/b3log/symphony/util/Pangu.java
create mode 100644 src/main/java/org/b3log/symphony/util/Results.java
create mode 100644 src/main/java/org/b3log/symphony/util/Runes.java
create mode 100644 src/main/java/org/b3log/symphony/util/Sessions.java
create mode 100644 src/main/java/org/b3log/symphony/util/StatusCodes.java
create mode 100644 src/main/java/org/b3log/symphony/util/Symphonys.java
create mode 100644 src/main/java/org/b3log/symphony/util/Templates.java
create mode 100644 src/main/java/org/b3log/symphony/util/Tesseracts.java
create mode 100644 src/main/java/org/b3log/symphony/util/VideoPlayers.java
create mode 100644 src/main/resources/CHANGE_LOGS.html
create mode 100644 src/main/resources/css/base.css
create mode 100644 src/main/resources/css/error.css
create mode 100644 src/main/resources/css/home.css
create mode 100644 src/main/resources/css/index.css
create mode 100644 src/main/resources/css/mobile-base.css
create mode 100644 src/main/resources/css/responsive.css
create mode 100644 src/main/resources/css/selection.json
create mode 100644 src/main/resources/docker/latke.properties
create mode 100644 src/main/resources/docker/local.properties
create mode 100644 src/main/resources/emoji/ZeroClipboard.swf
create mode 100644 src/main/resources/emoji/favicon.ico
create mode 100644 src/main/resources/emoji/graphics/+1.png
create mode 100644 src/main/resources/emoji/graphics/-1.png
create mode 100644 src/main/resources/emoji/graphics/100.png
create mode 100644 src/main/resources/emoji/graphics/1234.png
create mode 100644 src/main/resources/emoji/graphics/263a.png
create mode 100644 src/main/resources/emoji/graphics/8ball.png
create mode 100644 src/main/resources/emoji/graphics/a.png
create mode 100644 src/main/resources/emoji/graphics/ab.png
create mode 100644 src/main/resources/emoji/graphics/abc.png
create mode 100644 src/main/resources/emoji/graphics/abcd.png
create mode 100644 src/main/resources/emoji/graphics/accept.png
create mode 100644 src/main/resources/emoji/graphics/aerial_tramway.png
create mode 100644 src/main/resources/emoji/graphics/airplane.png
create mode 100644 src/main/resources/emoji/graphics/alarm_clock.png
create mode 100644 src/main/resources/emoji/graphics/alien.png
create mode 100644 src/main/resources/emoji/graphics/ambulance.png
create mode 100644 src/main/resources/emoji/graphics/anchor.png
create mode 100644 src/main/resources/emoji/graphics/angel.png
create mode 100644 src/main/resources/emoji/graphics/anger.png
create mode 100644 src/main/resources/emoji/graphics/angry.png
create mode 100644 src/main/resources/emoji/graphics/anguished.png
create mode 100644 src/main/resources/emoji/graphics/ant.png
create mode 100644 src/main/resources/emoji/graphics/apple.png
create mode 100644 src/main/resources/emoji/graphics/aquarius.png
create mode 100644 src/main/resources/emoji/graphics/aries.png
create mode 100644 src/main/resources/emoji/graphics/arrow_backward.png
create mode 100644 src/main/resources/emoji/graphics/arrow_double_down.png
create mode 100644 src/main/resources/emoji/graphics/arrow_double_up.png
create mode 100644 src/main/resources/emoji/graphics/arrow_down.png
create mode 100644 src/main/resources/emoji/graphics/arrow_down_small.png
create mode 100644 src/main/resources/emoji/graphics/arrow_forward.png
create mode 100644 src/main/resources/emoji/graphics/arrow_heading_down.png
create mode 100644 src/main/resources/emoji/graphics/arrow_heading_up.png
create mode 100644 src/main/resources/emoji/graphics/arrow_left.png
create mode 100644 src/main/resources/emoji/graphics/arrow_lower_left.png
create mode 100644 src/main/resources/emoji/graphics/arrow_lower_right.png
create mode 100644 src/main/resources/emoji/graphics/arrow_right.png
create mode 100644 src/main/resources/emoji/graphics/arrow_right_hook.png
create mode 100644 src/main/resources/emoji/graphics/arrow_up.png
create mode 100644 src/main/resources/emoji/graphics/arrow_up_down.png
create mode 100644 src/main/resources/emoji/graphics/arrow_up_small.png
create mode 100644 src/main/resources/emoji/graphics/arrow_upper_left.png
create mode 100644 src/main/resources/emoji/graphics/arrow_upper_right.png
create mode 100644 src/main/resources/emoji/graphics/arrows_clockwise.png
create mode 100644 src/main/resources/emoji/graphics/arrows_counterclockwise.png
create mode 100644 src/main/resources/emoji/graphics/art.png
create mode 100644 src/main/resources/emoji/graphics/articulated_lorry.png
create mode 100644 src/main/resources/emoji/graphics/astonished.png
create mode 100644 src/main/resources/emoji/graphics/atm.png
create mode 100644 src/main/resources/emoji/graphics/b.png
create mode 100644 src/main/resources/emoji/graphics/baby.png
create mode 100644 src/main/resources/emoji/graphics/baby_bottle.png
create mode 100644 src/main/resources/emoji/graphics/baby_chick.png
create mode 100644 src/main/resources/emoji/graphics/baby_symbol.png
create mode 100644 src/main/resources/emoji/graphics/back.png
create mode 100644 src/main/resources/emoji/graphics/baggage_claim.png
create mode 100644 src/main/resources/emoji/graphics/balloon.png
create mode 100644 src/main/resources/emoji/graphics/ballot_box_with_check.png
create mode 100644 src/main/resources/emoji/graphics/bamboo.png
create mode 100644 src/main/resources/emoji/graphics/banana.png
create mode 100644 src/main/resources/emoji/graphics/bangbang.png
create mode 100644 src/main/resources/emoji/graphics/bank.png
create mode 100644 src/main/resources/emoji/graphics/bar_chart.png
create mode 100644 src/main/resources/emoji/graphics/barber.png
create mode 100644 src/main/resources/emoji/graphics/baseball.png
create mode 100644 src/main/resources/emoji/graphics/basketball.png
create mode 100644 src/main/resources/emoji/graphics/bath.png
create mode 100644 src/main/resources/emoji/graphics/bathtub.png
create mode 100644 src/main/resources/emoji/graphics/battery.png
create mode 100644 src/main/resources/emoji/graphics/bear.png
create mode 100644 src/main/resources/emoji/graphics/bee.png
create mode 100644 src/main/resources/emoji/graphics/beer.png
create mode 100644 src/main/resources/emoji/graphics/beers.png
create mode 100644 src/main/resources/emoji/graphics/beetle.png
create mode 100644 src/main/resources/emoji/graphics/beginner.png
create mode 100644 src/main/resources/emoji/graphics/bell.png
create mode 100644 src/main/resources/emoji/graphics/bento.png
create mode 100644 src/main/resources/emoji/graphics/bicyclist.png
create mode 100644 src/main/resources/emoji/graphics/bike.png
create mode 100644 src/main/resources/emoji/graphics/bikini.png
create mode 100644 src/main/resources/emoji/graphics/bird.png
create mode 100644 src/main/resources/emoji/graphics/birthday.png
create mode 100644 src/main/resources/emoji/graphics/black_circle.png
create mode 100644 src/main/resources/emoji/graphics/black_joker.png
create mode 100644 src/main/resources/emoji/graphics/black_large_square.png
create mode 100644 src/main/resources/emoji/graphics/black_medium_small_square.png
create mode 100644 src/main/resources/emoji/graphics/black_medium_square.png
create mode 100644 src/main/resources/emoji/graphics/black_nib.png
create mode 100644 src/main/resources/emoji/graphics/black_small_square.png
create mode 100644 src/main/resources/emoji/graphics/black_square_button.png
create mode 100644 src/main/resources/emoji/graphics/blossom.png
create mode 100644 src/main/resources/emoji/graphics/blowfish.png
create mode 100644 src/main/resources/emoji/graphics/blue_book.png
create mode 100644 src/main/resources/emoji/graphics/blue_car.png
create mode 100644 src/main/resources/emoji/graphics/blue_heart.png
create mode 100644 src/main/resources/emoji/graphics/blush.png
create mode 100644 src/main/resources/emoji/graphics/boar.png
create mode 100644 src/main/resources/emoji/graphics/boat.png
create mode 100644 src/main/resources/emoji/graphics/bomb.png
create mode 100644 src/main/resources/emoji/graphics/book.png
create mode 100644 src/main/resources/emoji/graphics/bookmark.png
create mode 100644 src/main/resources/emoji/graphics/bookmark_tabs.png
create mode 100644 src/main/resources/emoji/graphics/books.png
create mode 100644 src/main/resources/emoji/graphics/boom.png
create mode 100644 src/main/resources/emoji/graphics/boot.png
create mode 100644 src/main/resources/emoji/graphics/bouquet.png
create mode 100644 src/main/resources/emoji/graphics/bow.png
create mode 100644 src/main/resources/emoji/graphics/bowling.png
create mode 100644 src/main/resources/emoji/graphics/boy.png
create mode 100644 src/main/resources/emoji/graphics/bread.png
create mode 100644 src/main/resources/emoji/graphics/bride_with_veil.png
create mode 100644 src/main/resources/emoji/graphics/bridge_at_night.png
create mode 100644 src/main/resources/emoji/graphics/briefcase.png
create mode 100644 src/main/resources/emoji/graphics/broken_heart.png
create mode 100644 src/main/resources/emoji/graphics/bug.png
create mode 100644 src/main/resources/emoji/graphics/bulb.png
create mode 100644 src/main/resources/emoji/graphics/bullettrain_front.png
create mode 100644 src/main/resources/emoji/graphics/bullettrain_side.png
create mode 100644 src/main/resources/emoji/graphics/bus.png
create mode 100644 src/main/resources/emoji/graphics/busstop.png
create mode 100644 src/main/resources/emoji/graphics/bust_in_silhouette.png
create mode 100644 src/main/resources/emoji/graphics/busts_in_silhouette.png
create mode 100644 src/main/resources/emoji/graphics/c.png
create mode 100644 src/main/resources/emoji/graphics/cactus.png
create mode 100644 src/main/resources/emoji/graphics/cake.png
create mode 100644 src/main/resources/emoji/graphics/calendar.png
create mode 100644 src/main/resources/emoji/graphics/calling.png
create mode 100644 src/main/resources/emoji/graphics/camel.png
create mode 100644 src/main/resources/emoji/graphics/camera.png
create mode 100644 src/main/resources/emoji/graphics/cancer.png
create mode 100644 src/main/resources/emoji/graphics/candy.png
create mode 100644 src/main/resources/emoji/graphics/capital_abcd.png
create mode 100644 src/main/resources/emoji/graphics/capricorn.png
create mode 100644 src/main/resources/emoji/graphics/car.png
create mode 100644 src/main/resources/emoji/graphics/card_index.png
create mode 100644 src/main/resources/emoji/graphics/carousel_horse.png
create mode 100644 src/main/resources/emoji/graphics/cat.png
create mode 100644 src/main/resources/emoji/graphics/cat2.png
create mode 100644 src/main/resources/emoji/graphics/cd.png
create mode 100644 src/main/resources/emoji/graphics/chart.png
create mode 100644 src/main/resources/emoji/graphics/chart_with_downwards_trend.png
create mode 100644 src/main/resources/emoji/graphics/chart_with_upwards_trend.png
create mode 100644 src/main/resources/emoji/graphics/checkered_flag.png
create mode 100644 src/main/resources/emoji/graphics/cherries.png
create mode 100644 src/main/resources/emoji/graphics/cherry_blossom.png
create mode 100644 src/main/resources/emoji/graphics/chestnut.png
create mode 100644 src/main/resources/emoji/graphics/chicken.png
create mode 100644 src/main/resources/emoji/graphics/children_crossing.png
create mode 100644 src/main/resources/emoji/graphics/chocolate_bar.png
create mode 100644 src/main/resources/emoji/graphics/christmas_tree.png
create mode 100644 src/main/resources/emoji/graphics/church.png
create mode 100644 src/main/resources/emoji/graphics/cinema.png
create mode 100644 src/main/resources/emoji/graphics/circus_tent.png
create mode 100644 src/main/resources/emoji/graphics/city_sunrise.png
create mode 100644 src/main/resources/emoji/graphics/city_sunset.png
create mode 100644 src/main/resources/emoji/graphics/cl.png
create mode 100644 src/main/resources/emoji/graphics/clap.png
create mode 100644 src/main/resources/emoji/graphics/clapper.png
create mode 100644 src/main/resources/emoji/graphics/clipboard.png
create mode 100644 src/main/resources/emoji/graphics/clock1.png
create mode 100644 src/main/resources/emoji/graphics/clock10.png
create mode 100644 src/main/resources/emoji/graphics/clock1030.png
create mode 100644 src/main/resources/emoji/graphics/clock11.png
create mode 100644 src/main/resources/emoji/graphics/clock1130.png
create mode 100644 src/main/resources/emoji/graphics/clock12.png
create mode 100644 src/main/resources/emoji/graphics/clock1230.png
create mode 100644 src/main/resources/emoji/graphics/clock130.png
create mode 100644 src/main/resources/emoji/graphics/clock2.png
create mode 100644 src/main/resources/emoji/graphics/clock230.png
create mode 100644 src/main/resources/emoji/graphics/clock3.png
create mode 100644 src/main/resources/emoji/graphics/clock330.png
create mode 100644 src/main/resources/emoji/graphics/clock4.png
create mode 100644 src/main/resources/emoji/graphics/clock430.png
create mode 100644 src/main/resources/emoji/graphics/clock5.png
create mode 100644 src/main/resources/emoji/graphics/clock530.png
create mode 100644 src/main/resources/emoji/graphics/clock6.png
create mode 100644 src/main/resources/emoji/graphics/clock630.png
create mode 100644 src/main/resources/emoji/graphics/clock7.png
create mode 100644 src/main/resources/emoji/graphics/clock730.png
create mode 100644 src/main/resources/emoji/graphics/clock8.png
create mode 100644 src/main/resources/emoji/graphics/clock830.png
create mode 100644 src/main/resources/emoji/graphics/clock9.png
create mode 100644 src/main/resources/emoji/graphics/clock930.png
create mode 100644 src/main/resources/emoji/graphics/closed_book.png
create mode 100644 src/main/resources/emoji/graphics/closed_lock_with_key.png
create mode 100644 src/main/resources/emoji/graphics/closed_umbrella.png
create mode 100644 src/main/resources/emoji/graphics/cloud.png
create mode 100644 src/main/resources/emoji/graphics/clubs.png
create mode 100644 src/main/resources/emoji/graphics/cn.png
create mode 100644 src/main/resources/emoji/graphics/cocktail.png
create mode 100644 src/main/resources/emoji/graphics/coffee.png
create mode 100644 src/main/resources/emoji/graphics/cold_sweat.png
create mode 100644 src/main/resources/emoji/graphics/collision.png
create mode 100644 src/main/resources/emoji/graphics/computer.png
create mode 100644 src/main/resources/emoji/graphics/confetti_ball.png
create mode 100644 src/main/resources/emoji/graphics/confounded.png
create mode 100644 src/main/resources/emoji/graphics/confused.png
create mode 100644 src/main/resources/emoji/graphics/congratulations.png
create mode 100644 src/main/resources/emoji/graphics/construction.png
create mode 100644 src/main/resources/emoji/graphics/construction_worker.png
create mode 100644 src/main/resources/emoji/graphics/convenience_store.png
create mode 100644 src/main/resources/emoji/graphics/cookie.png
create mode 100644 src/main/resources/emoji/graphics/cool.png
create mode 100644 src/main/resources/emoji/graphics/cop.png
create mode 100644 src/main/resources/emoji/graphics/copyright.png
create mode 100644 src/main/resources/emoji/graphics/corn.png
create mode 100644 src/main/resources/emoji/graphics/couple.png
create mode 100644 src/main/resources/emoji/graphics/couple_with_heart.png
create mode 100644 src/main/resources/emoji/graphics/couplekiss.png
create mode 100644 src/main/resources/emoji/graphics/cow.png
create mode 100644 src/main/resources/emoji/graphics/cow2.png
create mode 100644 src/main/resources/emoji/graphics/credit_card.png
create mode 100644 src/main/resources/emoji/graphics/crescent_moon.png
create mode 100644 src/main/resources/emoji/graphics/crocodile.png
create mode 100644 src/main/resources/emoji/graphics/crossed_flags.png
create mode 100644 src/main/resources/emoji/graphics/crown.png
create mode 100644 src/main/resources/emoji/graphics/cry.png
create mode 100644 src/main/resources/emoji/graphics/crying_cat_face.png
create mode 100644 src/main/resources/emoji/graphics/crystal_ball.png
create mode 100644 src/main/resources/emoji/graphics/cupid.png
create mode 100644 src/main/resources/emoji/graphics/curly_loop.png
create mode 100644 src/main/resources/emoji/graphics/currency_exchange.png
create mode 100644 src/main/resources/emoji/graphics/curry.png
create mode 100644 src/main/resources/emoji/graphics/custard.png
create mode 100644 src/main/resources/emoji/graphics/customs.png
create mode 100644 src/main/resources/emoji/graphics/cyclone.png
create mode 100644 src/main/resources/emoji/graphics/d.png
create mode 100644 src/main/resources/emoji/graphics/dancer.png
create mode 100644 src/main/resources/emoji/graphics/dancers.png
create mode 100644 src/main/resources/emoji/graphics/dango.png
create mode 100644 src/main/resources/emoji/graphics/dart.png
create mode 100644 src/main/resources/emoji/graphics/dash.png
create mode 100644 src/main/resources/emoji/graphics/date.png
create mode 100644 src/main/resources/emoji/graphics/de.png
create mode 100644 src/main/resources/emoji/graphics/deciduous_tree.png
create mode 100644 src/main/resources/emoji/graphics/department_store.png
create mode 100644 src/main/resources/emoji/graphics/diamond_shape_with_a_dot_inside.png
create mode 100644 src/main/resources/emoji/graphics/diamonds.png
create mode 100644 src/main/resources/emoji/graphics/disappointed.png
create mode 100644 src/main/resources/emoji/graphics/disappointed_relieved.png
create mode 100644 src/main/resources/emoji/graphics/dizzy.png
create mode 100644 src/main/resources/emoji/graphics/dizzy_face.png
create mode 100644 src/main/resources/emoji/graphics/do_not_litter.png
create mode 100644 src/main/resources/emoji/graphics/dog.png
create mode 100644 src/main/resources/emoji/graphics/dog2.png
create mode 100644 src/main/resources/emoji/graphics/doge.png
create mode 100644 src/main/resources/emoji/graphics/dollar.png
create mode 100644 src/main/resources/emoji/graphics/dolls.png
create mode 100644 src/main/resources/emoji/graphics/dolphin.png
create mode 100644 src/main/resources/emoji/graphics/door.png
create mode 100644 src/main/resources/emoji/graphics/doughnut.png
create mode 100644 src/main/resources/emoji/graphics/dragon.png
create mode 100644 src/main/resources/emoji/graphics/dragon_face.png
create mode 100644 src/main/resources/emoji/graphics/dress.png
create mode 100644 src/main/resources/emoji/graphics/dromedary_camel.png
create mode 100644 src/main/resources/emoji/graphics/droplet.png
create mode 100644 src/main/resources/emoji/graphics/dvd.png
create mode 100644 src/main/resources/emoji/graphics/e-mail.png
create mode 100644 src/main/resources/emoji/graphics/e50a.png
create mode 100644 src/main/resources/emoji/graphics/ear.png
create mode 100644 src/main/resources/emoji/graphics/ear_of_rice.png
create mode 100644 src/main/resources/emoji/graphics/earth_africa.png
create mode 100644 src/main/resources/emoji/graphics/earth_americas.png
create mode 100644 src/main/resources/emoji/graphics/earth_asia.png
create mode 100644 src/main/resources/emoji/graphics/egg.png
create mode 100644 src/main/resources/emoji/graphics/eggplant.png
create mode 100644 src/main/resources/emoji/graphics/eight.png
create mode 100644 src/main/resources/emoji/graphics/eight_pointed_black_star.png
create mode 100644 src/main/resources/emoji/graphics/eight_spoked_asterisk.png
create mode 100644 src/main/resources/emoji/graphics/electric_plug.png
create mode 100644 src/main/resources/emoji/graphics/elephant.png
create mode 100644 src/main/resources/emoji/graphics/email.png
create mode 100644 src/main/resources/emoji/graphics/end.png
create mode 100644 src/main/resources/emoji/graphics/envelope.png
create mode 100644 src/main/resources/emoji/graphics/es.png
create mode 100644 src/main/resources/emoji/graphics/euro.png
create mode 100644 src/main/resources/emoji/graphics/european_castle.png
create mode 100644 src/main/resources/emoji/graphics/european_post_office.png
create mode 100644 src/main/resources/emoji/graphics/evergreen_tree.png
create mode 100644 src/main/resources/emoji/graphics/exclamation.png
create mode 100644 src/main/resources/emoji/graphics/expressionless.png
create mode 100644 src/main/resources/emoji/graphics/eyeglasses.png
create mode 100644 src/main/resources/emoji/graphics/eyes.png
create mode 100644 src/main/resources/emoji/graphics/f.png
create mode 100644 src/main/resources/emoji/graphics/facepunch.png
create mode 100644 src/main/resources/emoji/graphics/factory.png
create mode 100644 src/main/resources/emoji/graphics/fallen_leaf.png
create mode 100644 src/main/resources/emoji/graphics/family.png
create mode 100644 src/main/resources/emoji/graphics/fast_forward.png
create mode 100644 src/main/resources/emoji/graphics/fax.png
create mode 100644 src/main/resources/emoji/graphics/fearful.png
create mode 100644 src/main/resources/emoji/graphics/feet.png
create mode 100644 src/main/resources/emoji/graphics/ferris_wheel.png
create mode 100644 src/main/resources/emoji/graphics/file_folder.png
create mode 100644 src/main/resources/emoji/graphics/fire.png
create mode 100644 src/main/resources/emoji/graphics/fire_engine.png
create mode 100644 src/main/resources/emoji/graphics/fireworks.png
create mode 100644 src/main/resources/emoji/graphics/first_quarter_moon.png
create mode 100644 src/main/resources/emoji/graphics/first_quarter_moon_with_face.png
create mode 100644 src/main/resources/emoji/graphics/fish.png
create mode 100644 src/main/resources/emoji/graphics/fish_cake.png
create mode 100644 src/main/resources/emoji/graphics/fishing_pole_and_fish.png
create mode 100644 src/main/resources/emoji/graphics/fist.png
create mode 100644 src/main/resources/emoji/graphics/five.png
create mode 100644 src/main/resources/emoji/graphics/flags.png
create mode 100644 src/main/resources/emoji/graphics/flashlight.png
create mode 100644 src/main/resources/emoji/graphics/floppy_disk.png
create mode 100644 src/main/resources/emoji/graphics/flower_playing_cards.png
create mode 100644 src/main/resources/emoji/graphics/flushed.png
create mode 100644 src/main/resources/emoji/graphics/foggy.png
create mode 100644 src/main/resources/emoji/graphics/football.png
create mode 100644 src/main/resources/emoji/graphics/fork_and_knife.png
create mode 100644 src/main/resources/emoji/graphics/fountain.png
create mode 100644 src/main/resources/emoji/graphics/four.png
create mode 100644 src/main/resources/emoji/graphics/four_leaf_clover.png
create mode 100644 src/main/resources/emoji/graphics/fr.png
create mode 100644 src/main/resources/emoji/graphics/free.png
create mode 100644 src/main/resources/emoji/graphics/fried_shrimp.png
create mode 100644 src/main/resources/emoji/graphics/fries.png
create mode 100644 src/main/resources/emoji/graphics/frog.png
create mode 100644 src/main/resources/emoji/graphics/frowning.png
create mode 100644 src/main/resources/emoji/graphics/fuelpump.png
create mode 100644 src/main/resources/emoji/graphics/full_moon.png
create mode 100644 src/main/resources/emoji/graphics/full_moon_with_face.png
create mode 100644 src/main/resources/emoji/graphics/g.png
create mode 100644 src/main/resources/emoji/graphics/game_die.png
create mode 100644 src/main/resources/emoji/graphics/gb.png
create mode 100644 src/main/resources/emoji/graphics/gem.png
create mode 100644 src/main/resources/emoji/graphics/gemini.png
create mode 100644 src/main/resources/emoji/graphics/ghost.png
create mode 100644 src/main/resources/emoji/graphics/gift.png
create mode 100644 src/main/resources/emoji/graphics/gift_heart.png
create mode 100644 src/main/resources/emoji/graphics/girl.png
create mode 100644 src/main/resources/emoji/graphics/globe_with_meridians.png
create mode 100644 src/main/resources/emoji/graphics/goat.png
create mode 100644 src/main/resources/emoji/graphics/golf.png
create mode 100644 src/main/resources/emoji/graphics/grapes.png
create mode 100644 src/main/resources/emoji/graphics/green_apple.png
create mode 100644 src/main/resources/emoji/graphics/green_book.png
create mode 100644 src/main/resources/emoji/graphics/green_heart.png
create mode 100644 src/main/resources/emoji/graphics/grey_exclamation.png
create mode 100644 src/main/resources/emoji/graphics/grey_question.png
create mode 100644 src/main/resources/emoji/graphics/grimacing.png
create mode 100644 src/main/resources/emoji/graphics/grin.png
create mode 100644 src/main/resources/emoji/graphics/grinning.png
create mode 100644 src/main/resources/emoji/graphics/guardsman.png
create mode 100644 src/main/resources/emoji/graphics/guitar.png
create mode 100644 src/main/resources/emoji/graphics/gun.png
create mode 100644 src/main/resources/emoji/graphics/haircut.png
create mode 100644 src/main/resources/emoji/graphics/hamburger.png
create mode 100644 src/main/resources/emoji/graphics/hammer.png
create mode 100644 src/main/resources/emoji/graphics/hamster.png
create mode 100644 src/main/resources/emoji/graphics/hand.png
create mode 100644 src/main/resources/emoji/graphics/handbag.png
create mode 100644 src/main/resources/emoji/graphics/hankey.png
create mode 100644 src/main/resources/emoji/graphics/hash.png
create mode 100644 src/main/resources/emoji/graphics/hatched_chick.png
create mode 100644 src/main/resources/emoji/graphics/hatching_chick.png
create mode 100644 src/main/resources/emoji/graphics/headphones.png
create mode 100644 src/main/resources/emoji/graphics/hear_no_evil.png
create mode 100644 src/main/resources/emoji/graphics/heart.png
create mode 100644 src/main/resources/emoji/graphics/heart_decoration.png
create mode 100644 src/main/resources/emoji/graphics/heart_eyes.png
create mode 100644 src/main/resources/emoji/graphics/heart_eyes_cat.png
create mode 100644 src/main/resources/emoji/graphics/heartbeat.png
create mode 100644 src/main/resources/emoji/graphics/heartpulse.png
create mode 100644 src/main/resources/emoji/graphics/hearts.png
create mode 100644 src/main/resources/emoji/graphics/heavy_check_mark.png
create mode 100644 src/main/resources/emoji/graphics/heavy_division_sign.png
create mode 100644 src/main/resources/emoji/graphics/heavy_dollar_sign.png
create mode 100644 src/main/resources/emoji/graphics/heavy_exclamation_mark.png
create mode 100644 src/main/resources/emoji/graphics/heavy_minus_sign.png
create mode 100644 src/main/resources/emoji/graphics/heavy_multiplication_x.png
create mode 100644 src/main/resources/emoji/graphics/heavy_plus_sign.png
create mode 100644 src/main/resources/emoji/graphics/helicopter.png
create mode 100644 src/main/resources/emoji/graphics/herb.png
create mode 100644 src/main/resources/emoji/graphics/hibiscus.png
create mode 100644 src/main/resources/emoji/graphics/high_brightness.png
create mode 100644 src/main/resources/emoji/graphics/high_heel.png
create mode 100644 src/main/resources/emoji/graphics/hocho.png
create mode 100644 src/main/resources/emoji/graphics/honey_pot.png
create mode 100644 src/main/resources/emoji/graphics/honeybee.png
create mode 100644 src/main/resources/emoji/graphics/horse.png
create mode 100644 src/main/resources/emoji/graphics/horse_racing.png
create mode 100644 src/main/resources/emoji/graphics/hospital.png
create mode 100644 src/main/resources/emoji/graphics/hotel.png
create mode 100644 src/main/resources/emoji/graphics/hotsprings.png
create mode 100644 src/main/resources/emoji/graphics/hourglass.png
create mode 100644 src/main/resources/emoji/graphics/hourglass_flowing_sand.png
create mode 100644 src/main/resources/emoji/graphics/house.png
create mode 100644 src/main/resources/emoji/graphics/house_with_garden.png
create mode 100644 src/main/resources/emoji/graphics/huaji.gif
create mode 100644 src/main/resources/emoji/graphics/hushed.png
create mode 100644 src/main/resources/emoji/graphics/i.png
create mode 100644 src/main/resources/emoji/graphics/ice_cream.png
create mode 100644 src/main/resources/emoji/graphics/icecream.png
create mode 100644 src/main/resources/emoji/graphics/id.png
create mode 100644 src/main/resources/emoji/graphics/ideograph_advantage.png
create mode 100644 src/main/resources/emoji/graphics/imp.png
create mode 100644 src/main/resources/emoji/graphics/inbox_tray.png
create mode 100644 src/main/resources/emoji/graphics/incoming_envelope.png
create mode 100644 src/main/resources/emoji/graphics/information_desk_person.png
create mode 100644 src/main/resources/emoji/graphics/information_source.png
create mode 100644 src/main/resources/emoji/graphics/innocent.png
create mode 100644 src/main/resources/emoji/graphics/interrobang.png
create mode 100644 src/main/resources/emoji/graphics/iphone.png
create mode 100644 src/main/resources/emoji/graphics/it.png
create mode 100644 src/main/resources/emoji/graphics/izakaya_lantern.png
create mode 100644 src/main/resources/emoji/graphics/j.png
create mode 100644 src/main/resources/emoji/graphics/jack_o_lantern.png
create mode 100644 src/main/resources/emoji/graphics/japan.png
create mode 100644 src/main/resources/emoji/graphics/japanese_castle.png
create mode 100644 src/main/resources/emoji/graphics/japanese_goblin.png
create mode 100644 src/main/resources/emoji/graphics/japanese_ogre.png
create mode 100644 src/main/resources/emoji/graphics/jeans.png
create mode 100644 src/main/resources/emoji/graphics/joy.png
create mode 100644 src/main/resources/emoji/graphics/joy_cat.png
create mode 100644 src/main/resources/emoji/graphics/jp.png
create mode 100644 src/main/resources/emoji/graphics/k.png
create mode 100644 src/main/resources/emoji/graphics/key.png
create mode 100644 src/main/resources/emoji/graphics/keycap_ten.png
create mode 100644 src/main/resources/emoji/graphics/kimono.png
create mode 100644 src/main/resources/emoji/graphics/kiss.png
create mode 100644 src/main/resources/emoji/graphics/kissing.png
create mode 100644 src/main/resources/emoji/graphics/kissing_cat.png
create mode 100644 src/main/resources/emoji/graphics/kissing_closed_eyes.png
create mode 100644 src/main/resources/emoji/graphics/kissing_heart.png
create mode 100644 src/main/resources/emoji/graphics/kissing_smiling_eyes.png
create mode 100644 src/main/resources/emoji/graphics/koala.png
create mode 100644 src/main/resources/emoji/graphics/koko.png
create mode 100644 src/main/resources/emoji/graphics/kr.png
create mode 100644 src/main/resources/emoji/graphics/large_blue_circle.png
create mode 100644 src/main/resources/emoji/graphics/large_blue_diamond.png
create mode 100644 src/main/resources/emoji/graphics/large_orange_diamond.png
create mode 100644 src/main/resources/emoji/graphics/last_quarter_moon.png
create mode 100644 src/main/resources/emoji/graphics/last_quarter_moon_with_face.png
create mode 100644 src/main/resources/emoji/graphics/laughing.png
create mode 100644 src/main/resources/emoji/graphics/leaves.png
create mode 100644 src/main/resources/emoji/graphics/ledger.png
create mode 100644 src/main/resources/emoji/graphics/left_luggage.png
create mode 100644 src/main/resources/emoji/graphics/left_right_arrow.png
create mode 100644 src/main/resources/emoji/graphics/leftwards_arrow_with_hook.png
create mode 100644 src/main/resources/emoji/graphics/lemon.png
create mode 100644 src/main/resources/emoji/graphics/leo.png
create mode 100644 src/main/resources/emoji/graphics/leopard.png
create mode 100644 src/main/resources/emoji/graphics/libra.png
create mode 100644 src/main/resources/emoji/graphics/light_rail.png
create mode 100644 src/main/resources/emoji/graphics/link.png
create mode 100644 src/main/resources/emoji/graphics/lips.png
create mode 100644 src/main/resources/emoji/graphics/lipstick.png
create mode 100644 src/main/resources/emoji/graphics/lock.png
create mode 100644 src/main/resources/emoji/graphics/lock_with_ink_pen.png
create mode 100644 src/main/resources/emoji/graphics/lollipop.png
create mode 100644 src/main/resources/emoji/graphics/loop.png
create mode 100644 src/main/resources/emoji/graphics/loudspeaker.png
create mode 100644 src/main/resources/emoji/graphics/love_hotel.png
create mode 100644 src/main/resources/emoji/graphics/love_letter.png
create mode 100644 src/main/resources/emoji/graphics/low_brightness.png
create mode 100644 src/main/resources/emoji/graphics/m.png
create mode 100644 src/main/resources/emoji/graphics/mag.png
create mode 100644 src/main/resources/emoji/graphics/mag_right.png
create mode 100644 src/main/resources/emoji/graphics/mahjong.png
create mode 100644 src/main/resources/emoji/graphics/mailbox.png
create mode 100644 src/main/resources/emoji/graphics/mailbox_closed.png
create mode 100644 src/main/resources/emoji/graphics/mailbox_with_mail.png
create mode 100644 src/main/resources/emoji/graphics/mailbox_with_no_mail.png
create mode 100644 src/main/resources/emoji/graphics/man.png
create mode 100644 src/main/resources/emoji/graphics/man_with_gua_pi_mao.png
create mode 100644 src/main/resources/emoji/graphics/man_with_turban.png
create mode 100644 src/main/resources/emoji/graphics/mans_shoe.png
create mode 100644 src/main/resources/emoji/graphics/maple_leaf.png
create mode 100644 src/main/resources/emoji/graphics/mask.png
create mode 100644 src/main/resources/emoji/graphics/massage.png
create mode 100644 src/main/resources/emoji/graphics/meat_on_bone.png
create mode 100644 src/main/resources/emoji/graphics/mega.png
create mode 100644 src/main/resources/emoji/graphics/melon.png
create mode 100644 src/main/resources/emoji/graphics/memo.png
create mode 100644 src/main/resources/emoji/graphics/mens.png
create mode 100644 src/main/resources/emoji/graphics/metro.png
create mode 100644 src/main/resources/emoji/graphics/microphone.png
create mode 100644 src/main/resources/emoji/graphics/microscope.png
create mode 100644 src/main/resources/emoji/graphics/milky_way.png
create mode 100644 src/main/resources/emoji/graphics/minibus.png
create mode 100644 src/main/resources/emoji/graphics/minidisc.png
create mode 100644 src/main/resources/emoji/graphics/mobile_phone_off.png
create mode 100644 src/main/resources/emoji/graphics/money_with_wings.png
create mode 100644 src/main/resources/emoji/graphics/moneybag.png
create mode 100644 src/main/resources/emoji/graphics/monkey.png
create mode 100644 src/main/resources/emoji/graphics/monkey_face.png
create mode 100644 src/main/resources/emoji/graphics/monorail.png
create mode 100644 src/main/resources/emoji/graphics/mortar_board.png
create mode 100644 src/main/resources/emoji/graphics/mount_fuji.png
create mode 100644 src/main/resources/emoji/graphics/mountain_bicyclist.png
create mode 100644 src/main/resources/emoji/graphics/mountain_cableway.png
create mode 100644 src/main/resources/emoji/graphics/mountain_railway.png
create mode 100644 src/main/resources/emoji/graphics/mouse.png
create mode 100644 src/main/resources/emoji/graphics/mouse2.png
create mode 100644 src/main/resources/emoji/graphics/movie_camera.png
create mode 100644 src/main/resources/emoji/graphics/moyai.png
create mode 100644 src/main/resources/emoji/graphics/muscle.png
create mode 100644 src/main/resources/emoji/graphics/mushroom.png
create mode 100644 src/main/resources/emoji/graphics/musical_keyboard.png
create mode 100644 src/main/resources/emoji/graphics/musical_note.png
create mode 100644 src/main/resources/emoji/graphics/musical_score.png
create mode 100644 src/main/resources/emoji/graphics/mute.png
create mode 100644 src/main/resources/emoji/graphics/nail_care.png
create mode 100644 src/main/resources/emoji/graphics/name_badge.png
create mode 100644 src/main/resources/emoji/graphics/necktie.png
create mode 100644 src/main/resources/emoji/graphics/negative_squared_cross_mark.png
create mode 100644 src/main/resources/emoji/graphics/neutral_face.png
create mode 100644 src/main/resources/emoji/graphics/new.png
create mode 100644 src/main/resources/emoji/graphics/new_moon.png
create mode 100644 src/main/resources/emoji/graphics/new_moon_with_face.png
create mode 100644 src/main/resources/emoji/graphics/newspaper.png
create mode 100644 src/main/resources/emoji/graphics/ng.png
create mode 100644 src/main/resources/emoji/graphics/nine.png
create mode 100644 src/main/resources/emoji/graphics/no_bell.png
create mode 100644 src/main/resources/emoji/graphics/no_bicycles.png
create mode 100644 src/main/resources/emoji/graphics/no_entry.png
create mode 100644 src/main/resources/emoji/graphics/no_entry_sign.png
create mode 100644 src/main/resources/emoji/graphics/no_good.png
create mode 100644 src/main/resources/emoji/graphics/no_mobile_phones.png
create mode 100644 src/main/resources/emoji/graphics/no_mouth.png
create mode 100644 src/main/resources/emoji/graphics/no_pedestrians.png
create mode 100644 src/main/resources/emoji/graphics/no_smoking.png
create mode 100644 src/main/resources/emoji/graphics/non-potable_water.png
create mode 100644 src/main/resources/emoji/graphics/nose.png
create mode 100644 src/main/resources/emoji/graphics/notebook.png
create mode 100644 src/main/resources/emoji/graphics/notebook_with_decorative_cover.png
create mode 100644 src/main/resources/emoji/graphics/notes.png
create mode 100644 src/main/resources/emoji/graphics/nut_and_bolt.png
create mode 100644 src/main/resources/emoji/graphics/o.png
create mode 100644 src/main/resources/emoji/graphics/o2.png
create mode 100644 src/main/resources/emoji/graphics/ocean.png
create mode 100644 src/main/resources/emoji/graphics/octocat.png
create mode 100644 src/main/resources/emoji/graphics/octopus.png
create mode 100644 src/main/resources/emoji/graphics/oden.png
create mode 100644 src/main/resources/emoji/graphics/office.png
create mode 100644 src/main/resources/emoji/graphics/ok.png
create mode 100644 src/main/resources/emoji/graphics/ok_hand.png
create mode 100644 src/main/resources/emoji/graphics/ok_woman.png
create mode 100644 src/main/resources/emoji/graphics/older_man.png
create mode 100644 src/main/resources/emoji/graphics/older_woman.png
create mode 100644 src/main/resources/emoji/graphics/on.png
create mode 100644 src/main/resources/emoji/graphics/oncoming_automobile.png
create mode 100644 src/main/resources/emoji/graphics/oncoming_bus.png
create mode 100644 src/main/resources/emoji/graphics/oncoming_police_car.png
create mode 100644 src/main/resources/emoji/graphics/oncoming_taxi.png
create mode 100644 src/main/resources/emoji/graphics/one.png
create mode 100644 src/main/resources/emoji/graphics/open_file_folder.png
create mode 100644 src/main/resources/emoji/graphics/open_hands.png
create mode 100644 src/main/resources/emoji/graphics/open_mouth.png
create mode 100644 src/main/resources/emoji/graphics/ophiuchus.png
create mode 100644 src/main/resources/emoji/graphics/orange_book.png
create mode 100644 src/main/resources/emoji/graphics/outbox_tray.png
create mode 100644 src/main/resources/emoji/graphics/ox.png
create mode 100644 src/main/resources/emoji/graphics/package.png
create mode 100644 src/main/resources/emoji/graphics/page_facing_up.png
create mode 100644 src/main/resources/emoji/graphics/page_with_curl.png
create mode 100644 src/main/resources/emoji/graphics/pager.png
create mode 100644 src/main/resources/emoji/graphics/palm_tree.png
create mode 100644 src/main/resources/emoji/graphics/panda_face.png
create mode 100644 src/main/resources/emoji/graphics/paperclip.png
create mode 100644 src/main/resources/emoji/graphics/parking.png
create mode 100644 src/main/resources/emoji/graphics/part_alternation_mark.png
create mode 100644 src/main/resources/emoji/graphics/partly_sunny.png
create mode 100644 src/main/resources/emoji/graphics/passport_control.png
create mode 100644 src/main/resources/emoji/graphics/paw_prints.png
create mode 100644 src/main/resources/emoji/graphics/peach.png
create mode 100644 src/main/resources/emoji/graphics/pear.png
create mode 100644 src/main/resources/emoji/graphics/pencil.png
create mode 100644 src/main/resources/emoji/graphics/pencil2.png
create mode 100644 src/main/resources/emoji/graphics/penguin.png
create mode 100644 src/main/resources/emoji/graphics/pensive.png
create mode 100644 src/main/resources/emoji/graphics/performing_arts.png
create mode 100644 src/main/resources/emoji/graphics/persevere.png
create mode 100644 src/main/resources/emoji/graphics/person_frowning.png
create mode 100644 src/main/resources/emoji/graphics/person_with_blond_hair.png
create mode 100644 src/main/resources/emoji/graphics/person_with_pouting_face.png
create mode 100644 src/main/resources/emoji/graphics/phone.png
create mode 100644 src/main/resources/emoji/graphics/pig.png
create mode 100644 src/main/resources/emoji/graphics/pig2.png
create mode 100644 src/main/resources/emoji/graphics/pig_nose.png
create mode 100644 src/main/resources/emoji/graphics/pill.png
create mode 100644 src/main/resources/emoji/graphics/pineapple.png
create mode 100644 src/main/resources/emoji/graphics/pisces.png
create mode 100644 src/main/resources/emoji/graphics/pizza.png
create mode 100644 src/main/resources/emoji/graphics/point_down.png
create mode 100644 src/main/resources/emoji/graphics/point_left.png
create mode 100644 src/main/resources/emoji/graphics/point_right.png
create mode 100644 src/main/resources/emoji/graphics/point_up.png
create mode 100644 src/main/resources/emoji/graphics/point_up_2.png
create mode 100644 src/main/resources/emoji/graphics/police_car.png
create mode 100644 src/main/resources/emoji/graphics/poodle.png
create mode 100644 src/main/resources/emoji/graphics/poop.png
create mode 100644 src/main/resources/emoji/graphics/postal_horn.png
create mode 100644 src/main/resources/emoji/graphics/postbox.png
create mode 100644 src/main/resources/emoji/graphics/potable_water.png
create mode 100644 src/main/resources/emoji/graphics/pouch.png
create mode 100644 src/main/resources/emoji/graphics/poultry_leg.png
create mode 100644 src/main/resources/emoji/graphics/pound.png
create mode 100644 src/main/resources/emoji/graphics/pouting_cat.png
create mode 100644 src/main/resources/emoji/graphics/pray.png
create mode 100644 src/main/resources/emoji/graphics/princess.png
create mode 100644 src/main/resources/emoji/graphics/punch.png
create mode 100644 src/main/resources/emoji/graphics/purple_heart.png
create mode 100644 src/main/resources/emoji/graphics/purse.png
create mode 100644 src/main/resources/emoji/graphics/pushpin.png
create mode 100644 src/main/resources/emoji/graphics/put_litter_in_its_place.png
create mode 100644 src/main/resources/emoji/graphics/question.png
create mode 100644 src/main/resources/emoji/graphics/r.png
create mode 100644 src/main/resources/emoji/graphics/rabbit.png
create mode 100644 src/main/resources/emoji/graphics/rabbit2.png
create mode 100644 src/main/resources/emoji/graphics/racehorse.png
create mode 100644 src/main/resources/emoji/graphics/radio.png
create mode 100644 src/main/resources/emoji/graphics/radio_button.png
create mode 100644 src/main/resources/emoji/graphics/rage.png
create mode 100644 src/main/resources/emoji/graphics/railway_car.png
create mode 100644 src/main/resources/emoji/graphics/rainbow.png
create mode 100644 src/main/resources/emoji/graphics/raised_hand.png
create mode 100644 src/main/resources/emoji/graphics/raised_hands.png
create mode 100644 src/main/resources/emoji/graphics/raising_hand.png
create mode 100644 src/main/resources/emoji/graphics/ram.png
create mode 100644 src/main/resources/emoji/graphics/ramen.png
create mode 100644 src/main/resources/emoji/graphics/rat.png
create mode 100644 src/main/resources/emoji/graphics/recycle.png
create mode 100644 src/main/resources/emoji/graphics/red_car.png
create mode 100644 src/main/resources/emoji/graphics/red_circle.png
create mode 100644 src/main/resources/emoji/graphics/registered.png
create mode 100644 src/main/resources/emoji/graphics/relaxed.png
create mode 100644 src/main/resources/emoji/graphics/relieved.png
create mode 100644 src/main/resources/emoji/graphics/repeat.png
create mode 100644 src/main/resources/emoji/graphics/repeat_one.png
create mode 100644 src/main/resources/emoji/graphics/restroom.png
create mode 100644 src/main/resources/emoji/graphics/revolving_hearts.png
create mode 100644 src/main/resources/emoji/graphics/rewind.png
create mode 100644 src/main/resources/emoji/graphics/ribbon.png
create mode 100644 src/main/resources/emoji/graphics/rice.png
create mode 100644 src/main/resources/emoji/graphics/rice_ball.png
create mode 100644 src/main/resources/emoji/graphics/rice_cracker.png
create mode 100644 src/main/resources/emoji/graphics/rice_scene.png
create mode 100644 src/main/resources/emoji/graphics/ring.png
create mode 100644 src/main/resources/emoji/graphics/rocket.png
create mode 100644 src/main/resources/emoji/graphics/roller_coaster.png
create mode 100644 src/main/resources/emoji/graphics/rooster.png
create mode 100644 src/main/resources/emoji/graphics/rose.png
create mode 100644 src/main/resources/emoji/graphics/rotating_light.png
create mode 100644 src/main/resources/emoji/graphics/round_pushpin.png
create mode 100644 src/main/resources/emoji/graphics/rowboat.png
create mode 100644 src/main/resources/emoji/graphics/ru.png
create mode 100644 src/main/resources/emoji/graphics/rugby_football.png
create mode 100644 src/main/resources/emoji/graphics/running.png
create mode 100644 src/main/resources/emoji/graphics/running_shirt_with_sash.png
create mode 100644 src/main/resources/emoji/graphics/sa.png
create mode 100644 src/main/resources/emoji/graphics/sagittarius.png
create mode 100644 src/main/resources/emoji/graphics/sailboat.png
create mode 100644 src/main/resources/emoji/graphics/sake.png
create mode 100644 src/main/resources/emoji/graphics/sandal.png
create mode 100644 src/main/resources/emoji/graphics/santa.png
create mode 100644 src/main/resources/emoji/graphics/satellite.png
create mode 100644 src/main/resources/emoji/graphics/satisfied.png
create mode 100644 src/main/resources/emoji/graphics/saxophone.png
create mode 100644 src/main/resources/emoji/graphics/school.png
create mode 100644 src/main/resources/emoji/graphics/school_satchel.png
create mode 100644 src/main/resources/emoji/graphics/scissors.png
create mode 100644 src/main/resources/emoji/graphics/scorpius.png
create mode 100644 src/main/resources/emoji/graphics/scream.png
create mode 100644 src/main/resources/emoji/graphics/scream_cat.png
create mode 100644 src/main/resources/emoji/graphics/scroll.png
create mode 100644 src/main/resources/emoji/graphics/seat.png
create mode 100644 src/main/resources/emoji/graphics/secret.png
create mode 100644 src/main/resources/emoji/graphics/see_no_evil.png
create mode 100644 src/main/resources/emoji/graphics/seedling.png
create mode 100644 src/main/resources/emoji/graphics/seven.png
create mode 100644 src/main/resources/emoji/graphics/shaved_ice.png
create mode 100644 src/main/resources/emoji/graphics/sheep.png
create mode 100644 src/main/resources/emoji/graphics/shell.png
create mode 100644 src/main/resources/emoji/graphics/ship.png
create mode 100644 src/main/resources/emoji/graphics/shirt.png
create mode 100644 src/main/resources/emoji/graphics/shoe.png
create mode 100644 src/main/resources/emoji/graphics/shower.png
create mode 100644 src/main/resources/emoji/graphics/signal_strength.png
create mode 100644 src/main/resources/emoji/graphics/six.png
create mode 100644 src/main/resources/emoji/graphics/six_pointed_star.png
create mode 100644 src/main/resources/emoji/graphics/ski.png
create mode 100644 src/main/resources/emoji/graphics/skull.png
create mode 100644 src/main/resources/emoji/graphics/sleeping.png
create mode 100644 src/main/resources/emoji/graphics/sleepy.png
create mode 100644 src/main/resources/emoji/graphics/slot_machine.png
create mode 100644 src/main/resources/emoji/graphics/small_blue_diamond.png
create mode 100644 src/main/resources/emoji/graphics/small_orange_diamond.png
create mode 100644 src/main/resources/emoji/graphics/small_red_triangle.png
create mode 100644 src/main/resources/emoji/graphics/small_red_triangle_down.png
create mode 100644 src/main/resources/emoji/graphics/smile.png
create mode 100644 src/main/resources/emoji/graphics/smile_cat.png
create mode 100644 src/main/resources/emoji/graphics/smiley.png
create mode 100644 src/main/resources/emoji/graphics/smiley_cat.png
create mode 100644 src/main/resources/emoji/graphics/smiling_imp.png
create mode 100644 src/main/resources/emoji/graphics/smirk.png
create mode 100644 src/main/resources/emoji/graphics/smirk_cat.png
create mode 100644 src/main/resources/emoji/graphics/smoking.png
create mode 100644 src/main/resources/emoji/graphics/snail.png
create mode 100644 src/main/resources/emoji/graphics/snake.png
create mode 100644 src/main/resources/emoji/graphics/snowboarder.png
create mode 100644 src/main/resources/emoji/graphics/snowflake.png
create mode 100644 src/main/resources/emoji/graphics/snowman.png
create mode 100644 src/main/resources/emoji/graphics/sob.png
create mode 100644 src/main/resources/emoji/graphics/soccer.png
create mode 100644 src/main/resources/emoji/graphics/soon.png
create mode 100644 src/main/resources/emoji/graphics/sos.png
create mode 100644 src/main/resources/emoji/graphics/sound.png
create mode 100644 src/main/resources/emoji/graphics/space_invader.png
create mode 100644 src/main/resources/emoji/graphics/spades.png
create mode 100644 src/main/resources/emoji/graphics/spaghetti.png
create mode 100644 src/main/resources/emoji/graphics/sparkle.png
create mode 100644 src/main/resources/emoji/graphics/sparkler.png
create mode 100644 src/main/resources/emoji/graphics/sparkles.png
create mode 100644 src/main/resources/emoji/graphics/sparkling_heart.png
create mode 100644 src/main/resources/emoji/graphics/speak_no_evil.png
create mode 100644 src/main/resources/emoji/graphics/speaker.png
create mode 100644 src/main/resources/emoji/graphics/speech_balloon.png
create mode 100644 src/main/resources/emoji/graphics/speedboat.png
create mode 100644 src/main/resources/emoji/graphics/squirrel.png
create mode 100644 src/main/resources/emoji/graphics/star.png
create mode 100644 src/main/resources/emoji/graphics/star2.png
create mode 100644 src/main/resources/emoji/graphics/stars.png
create mode 100644 src/main/resources/emoji/graphics/station.png
create mode 100644 src/main/resources/emoji/graphics/statue_of_liberty.png
create mode 100644 src/main/resources/emoji/graphics/steam_locomotive.png
create mode 100644 src/main/resources/emoji/graphics/stew.png
create mode 100644 src/main/resources/emoji/graphics/straight_ruler.png
create mode 100644 src/main/resources/emoji/graphics/strawberry.png
create mode 100644 src/main/resources/emoji/graphics/stuck_out_tongue.png
create mode 100644 src/main/resources/emoji/graphics/stuck_out_tongue_closed_eyes.png
create mode 100644 src/main/resources/emoji/graphics/stuck_out_tongue_winking_eye.png
create mode 100644 src/main/resources/emoji/graphics/sun_with_face.png
create mode 100644 src/main/resources/emoji/graphics/sunflower.png
create mode 100644 src/main/resources/emoji/graphics/sunglasses.png
create mode 100644 src/main/resources/emoji/graphics/sunny.png
create mode 100644 src/main/resources/emoji/graphics/sunrise.png
create mode 100644 src/main/resources/emoji/graphics/sunrise_over_mountains.png
create mode 100644 src/main/resources/emoji/graphics/surfer.png
create mode 100644 src/main/resources/emoji/graphics/sushi.png
create mode 100644 src/main/resources/emoji/graphics/suspension_railway.png
create mode 100644 src/main/resources/emoji/graphics/sweat.png
create mode 100644 src/main/resources/emoji/graphics/sweat_drops.png
create mode 100644 src/main/resources/emoji/graphics/sweat_smile.png
create mode 100644 src/main/resources/emoji/graphics/sweet_potato.png
create mode 100644 src/main/resources/emoji/graphics/swimmer.png
create mode 100644 src/main/resources/emoji/graphics/symbols.png
create mode 100644 src/main/resources/emoji/graphics/syringe.png
create mode 100644 src/main/resources/emoji/graphics/tada.png
create mode 100644 src/main/resources/emoji/graphics/tanabata_tree.png
create mode 100644 src/main/resources/emoji/graphics/tangerine.png
create mode 100644 src/main/resources/emoji/graphics/taurus.png
create mode 100644 src/main/resources/emoji/graphics/taxi.png
create mode 100644 src/main/resources/emoji/graphics/tea.png
create mode 100644 src/main/resources/emoji/graphics/telephone.png
create mode 100644 src/main/resources/emoji/graphics/telephone_receiver.png
create mode 100644 src/main/resources/emoji/graphics/telescope.png
create mode 100644 src/main/resources/emoji/graphics/tennis.png
create mode 100644 src/main/resources/emoji/graphics/tent.png
create mode 100644 src/main/resources/emoji/graphics/thought_balloon.png
create mode 100644 src/main/resources/emoji/graphics/three.png
create mode 100644 src/main/resources/emoji/graphics/thumbsdown.png
create mode 100644 src/main/resources/emoji/graphics/thumbsup.png
create mode 100644 src/main/resources/emoji/graphics/ticket.png
create mode 100644 src/main/resources/emoji/graphics/tiger.png
create mode 100644 src/main/resources/emoji/graphics/tiger2.png
create mode 100644 src/main/resources/emoji/graphics/tired_face.png
create mode 100644 src/main/resources/emoji/graphics/tm.png
create mode 100644 src/main/resources/emoji/graphics/toilet.png
create mode 100644 src/main/resources/emoji/graphics/tokyo_tower.png
create mode 100644 src/main/resources/emoji/graphics/tomato.png
create mode 100644 src/main/resources/emoji/graphics/tongue.png
create mode 100644 src/main/resources/emoji/graphics/top.png
create mode 100644 src/main/resources/emoji/graphics/tophat.png
create mode 100644 src/main/resources/emoji/graphics/tractor.png
create mode 100644 src/main/resources/emoji/graphics/traffic_light.png
create mode 100644 src/main/resources/emoji/graphics/train.png
create mode 100644 src/main/resources/emoji/graphics/train2.png
create mode 100644 src/main/resources/emoji/graphics/tram.png
create mode 100644 src/main/resources/emoji/graphics/triangular_flag_on_post.png
create mode 100644 src/main/resources/emoji/graphics/triangular_ruler.png
create mode 100644 src/main/resources/emoji/graphics/trident.png
create mode 100644 src/main/resources/emoji/graphics/triumph.png
create mode 100644 src/main/resources/emoji/graphics/trolleybus.png
create mode 100644 src/main/resources/emoji/graphics/trollface.png
create mode 100644 src/main/resources/emoji/graphics/trophy.png
create mode 100644 src/main/resources/emoji/graphics/tropical_drink.png
create mode 100644 src/main/resources/emoji/graphics/tropical_fish.png
create mode 100644 src/main/resources/emoji/graphics/truck.png
create mode 100644 src/main/resources/emoji/graphics/trumpet.png
create mode 100644 src/main/resources/emoji/graphics/tshirt.png
create mode 100644 src/main/resources/emoji/graphics/tulip.png
create mode 100644 src/main/resources/emoji/graphics/turtle.png
create mode 100644 src/main/resources/emoji/graphics/tv.png
create mode 100644 src/main/resources/emoji/graphics/twisted_rightwards_arrows.png
create mode 100644 src/main/resources/emoji/graphics/two.png
create mode 100644 src/main/resources/emoji/graphics/two_hearts.png
create mode 100644 src/main/resources/emoji/graphics/two_men_holding_hands.png
create mode 100644 src/main/resources/emoji/graphics/two_women_holding_hands.png
create mode 100644 src/main/resources/emoji/graphics/u.png
create mode 100644 src/main/resources/emoji/graphics/u5272.png
create mode 100644 src/main/resources/emoji/graphics/u5408.png
create mode 100644 src/main/resources/emoji/graphics/u55b6.png
create mode 100644 src/main/resources/emoji/graphics/u6307.png
create mode 100644 src/main/resources/emoji/graphics/u6708.png
create mode 100644 src/main/resources/emoji/graphics/u6709.png
create mode 100644 src/main/resources/emoji/graphics/u6e80.png
create mode 100644 src/main/resources/emoji/graphics/u7121.png
create mode 100644 src/main/resources/emoji/graphics/u7533.png
create mode 100644 src/main/resources/emoji/graphics/u7981.png
create mode 100644 src/main/resources/emoji/graphics/u7a7a.png
create mode 100644 src/main/resources/emoji/graphics/umbrella.png
create mode 100644 src/main/resources/emoji/graphics/unamused.png
create mode 100644 src/main/resources/emoji/graphics/underage.png
create mode 100644 src/main/resources/emoji/graphics/unicorn_face.png
create mode 100644 src/main/resources/emoji/graphics/unlock.png
create mode 100644 src/main/resources/emoji/graphics/up.png
create mode 100644 src/main/resources/emoji/graphics/us.png
create mode 100644 src/main/resources/emoji/graphics/v.png
create mode 100644 src/main/resources/emoji/graphics/vertical_traffic_light.png
create mode 100644 src/main/resources/emoji/graphics/vhs.png
create mode 100644 src/main/resources/emoji/graphics/vibration_mode.png
create mode 100644 src/main/resources/emoji/graphics/video_camera.png
create mode 100644 src/main/resources/emoji/graphics/video_game.png
create mode 100644 src/main/resources/emoji/graphics/violin.png
create mode 100644 src/main/resources/emoji/graphics/virgo.png
create mode 100644 src/main/resources/emoji/graphics/volcano.png
create mode 100644 src/main/resources/emoji/graphics/vs.png
create mode 100644 src/main/resources/emoji/graphics/walking.png
create mode 100644 src/main/resources/emoji/graphics/waning_crescent_moon.png
create mode 100644 src/main/resources/emoji/graphics/waning_gibbous_moon.png
create mode 100644 src/main/resources/emoji/graphics/warning.png
create mode 100644 src/main/resources/emoji/graphics/watch.png
create mode 100644 src/main/resources/emoji/graphics/water_buffalo.png
create mode 100644 src/main/resources/emoji/graphics/watermelon.png
create mode 100644 src/main/resources/emoji/graphics/wave.png
create mode 100644 src/main/resources/emoji/graphics/wavy_dash.png
create mode 100644 src/main/resources/emoji/graphics/waxing_crescent_moon.png
create mode 100644 src/main/resources/emoji/graphics/waxing_gibbous_moon.png
create mode 100644 src/main/resources/emoji/graphics/wc.png
create mode 100644 src/main/resources/emoji/graphics/weary.png
create mode 100644 src/main/resources/emoji/graphics/wedding.png
create mode 100644 src/main/resources/emoji/graphics/whale.png
create mode 100644 src/main/resources/emoji/graphics/whale2.png
create mode 100644 src/main/resources/emoji/graphics/wheelchair.png
create mode 100644 src/main/resources/emoji/graphics/white_check_mark.png
create mode 100644 src/main/resources/emoji/graphics/white_circle.png
create mode 100644 src/main/resources/emoji/graphics/white_flower.png
create mode 100644 src/main/resources/emoji/graphics/white_large_square.png
create mode 100644 src/main/resources/emoji/graphics/white_medium_small_square.png
create mode 100644 src/main/resources/emoji/graphics/white_medium_square.png
create mode 100644 src/main/resources/emoji/graphics/white_small_square.png
create mode 100644 src/main/resources/emoji/graphics/white_square_button.png
create mode 100644 src/main/resources/emoji/graphics/wind_chime.png
create mode 100644 src/main/resources/emoji/graphics/wine_glass.png
create mode 100644 src/main/resources/emoji/graphics/wink.png
create mode 100644 src/main/resources/emoji/graphics/wolf.png
create mode 100644 src/main/resources/emoji/graphics/woman.png
create mode 100644 src/main/resources/emoji/graphics/womans_clothes.png
create mode 100644 src/main/resources/emoji/graphics/womans_hat.png
create mode 100644 src/main/resources/emoji/graphics/womens.png
create mode 100644 src/main/resources/emoji/graphics/worried.png
create mode 100644 src/main/resources/emoji/graphics/wrench.png
create mode 100644 src/main/resources/emoji/graphics/x.png
create mode 100644 src/main/resources/emoji/graphics/yellow_heart.png
create mode 100644 src/main/resources/emoji/graphics/yen.png
create mode 100644 src/main/resources/emoji/graphics/yum.png
create mode 100644 src/main/resources/emoji/graphics/zap.png
create mode 100644 src/main/resources/emoji/graphics/zero.png
create mode 100644 src/main/resources/emoji/graphics/zzz.png
create mode 100644 src/main/resources/emoji/index.html
create mode 100644 src/main/resources/emoji/script.js
create mode 100644 src/main/resources/emoji/style.css
create mode 100644 src/main/resources/etc/header.txt
create mode 100644 src/main/resources/halt.html
create mode 100644 src/main/resources/images/404/0.gif
create mode 100644 src/main/resources/images/404/1.gif
create mode 100644 src/main/resources/images/404/2.gif
create mode 100644 src/main/resources/images/404/3.gif
create mode 100644 src/main/resources/images/404/4.gif
create mode 100644 src/main/resources/images/404/5.gif
create mode 100644 src/main/resources/images/404/6.gif
create mode 100644 src/main/resources/images/H-20.png
create mode 100644 src/main/resources/images/activities/1A0001.png
create mode 100644 src/main/resources/images/activities/char.png
create mode 100644 src/main/resources/images/activities/chat.png
create mode 100644 src/main/resources/images/activities/checkin.png
create mode 100644 src/main/resources/images/activities/gobang.png
create mode 100644 src/main/resources/images/activities/snak.png
create mode 100644 src/main/resources/images/activities/yesterday.png
create mode 100644 src/main/resources/images/alipay-donate.png
create mode 100644 src/main/resources/images/blank.png
create mode 100644 src/main/resources/images/close.png
create mode 100644 src/main/resources/images/code-bg.png
create mode 100644 src/main/resources/images/favicon.png
create mode 100644 src/main/resources/images/faviconH.png
create mode 100644 src/main/resources/images/hacpai.png
create mode 100644 src/main/resources/images/holiday/book-bg.jpg
create mode 100644 src/main/resources/images/holiday/mc-banner.jpg
create mode 100644 src/main/resources/images/holiday/mc-bg.png
create mode 100644 src/main/resources/images/holiday/ny-bg.jpg
create mode 100644 src/main/resources/images/holiday/ny-logo.png
create mode 100644 src/main/resources/images/index-bg.svg
create mode 100644 src/main/resources/images/kill-browser.png
create mode 100644 src/main/resources/images/livephoto.png
create mode 100644 src/main/resources/images/logo.png
create mode 100644 src/main/resources/images/m-char.png
create mode 100644 src/main/resources/images/mail/verify-banner1.png
create mode 100644 src/main/resources/images/mail/weekly-banner1.png
create mode 100644 src/main/resources/images/mail/weekly-banner2.png
create mode 100644 src/main/resources/images/mail/weekly-banner3.png
create mode 100644 src/main/resources/images/music.png
create mode 100644 src/main/resources/images/robot_avatar.jpg
create mode 100644 src/main/resources/images/services/algolia128x40.png
create mode 100644 src/main/resources/images/sym-logo300.png
create mode 100644 src/main/resources/images/tags/sym.png
create mode 100644 src/main/resources/images/user-thumbnail.png
create mode 100644 src/main/resources/js/activity.js
create mode 100644 src/main/resources/js/activity.min.js
create mode 100644 src/main/resources/js/add-article.js
create mode 100644 src/main/resources/js/add-article.min.js
create mode 100644 src/main/resources/js/article.js
create mode 100644 src/main/resources/js/article.min.js
create mode 100644 src/main/resources/js/breezemoon.js
create mode 100644 src/main/resources/js/breezemoon.min.js
create mode 100644 src/main/resources/js/channel.js
create mode 100644 src/main/resources/js/channel.min.js
create mode 100644 src/main/resources/js/chat-room.js
create mode 100644 src/main/resources/js/chat-room.min.js
create mode 100644 src/main/resources/js/common.js
create mode 100644 src/main/resources/js/common.min.js
create mode 100644 src/main/resources/js/eating-snake.js
create mode 100644 src/main/resources/js/eating-snake.min.js
create mode 100644 src/main/resources/js/gobang.js
create mode 100644 src/main/resources/js/gobang.min.js
create mode 100644 src/main/resources/js/lib/algolia/algolia.min.js
create mode 100644 src/main/resources/js/lib/algolia/algoliasearch.min.js
create mode 100644 src/main/resources/js/lib/algolia/autocomplete.jquery.min.js
create mode 100644 src/main/resources/js/lib/aplayer/APlayer.min.js
create mode 100644 src/main/resources/js/lib/compress/article-libs.min.js
create mode 100644 src/main/resources/js/lib/compress/article.min.css
create mode 100644 src/main/resources/js/lib/compress/libs.min.js
create mode 100644 src/main/resources/js/lib/diff2html/diff.min.js
create mode 100644 src/main/resources/js/lib/diff2html/diff2html-ui.min.js
create mode 100644 src/main/resources/js/lib/diff2html/diff2html.min.css
create mode 100644 src/main/resources/js/lib/diff2html/diff2html.min.js
create mode 100644 src/main/resources/js/lib/echarts-2.2.7/chart/line.js
create mode 100644 src/main/resources/js/lib/echarts-2.2.7/echarts.js
create mode 100644 src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload-process.js
create mode 100644 src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload-validate.js
create mode 100644 src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload.js
create mode 100644 src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload.min.js
create mode 100644 src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.iframe-transport.js
create mode 100644 src/main/resources/js/lib/jquery/file-upload-9.10.1/vendor/jquery.ui.widget.js
create mode 100644 src/main/resources/js/lib/jquery/isotope.pkgd.min.js
create mode 100644 src/main/resources/js/lib/jquery/jquery-3.1.0.min.js
create mode 100644 src/main/resources/js/lib/jquery/jquery.bowknot.min.js
create mode 100644 src/main/resources/js/lib/jquery/jquery.hotkeys.js
create mode 100644 src/main/resources/js/lib/jquery/jquery.linkify-1.0-min.js
create mode 100644 src/main/resources/js/lib/jquery/jquery.pjax.js
create mode 100644 src/main/resources/js/lib/jquery/jquery.qrcode.min.js
create mode 100644 src/main/resources/js/lib/livephotoskit.js
create mode 100644 src/main/resources/js/lib/md5.js
create mode 100644 src/main/resources/js/lib/nprogress/nprogress.css
create mode 100644 src/main/resources/js/lib/nprogress/nprogress.js
create mode 100644 src/main/resources/js/lib/reconnecting-websocket.min.js
create mode 100644 src/main/resources/js/lib/sound-recorder/SoundRecorder.js
create mode 100644 src/main/resources/js/lib/ua-parser.min.js
create mode 100644 src/main/resources/js/m-article.js
create mode 100644 src/main/resources/js/m-article.min.js
create mode 100644 src/main/resources/js/settings.js
create mode 100644 src/main/resources/js/settings.min.js
create mode 100644 src/main/resources/js/symbol-defs.js
create mode 100644 src/main/resources/js/symbol-defs.min.js
create mode 100644 src/main/resources/js/verify.js
create mode 100644 src/main/resources/js/verify.min.js
create mode 100644 src/main/resources/lang_en_US.properties
create mode 100644 src/main/resources/lang_zh_CN.properties
create mode 100644 src/main/resources/latke.properties
create mode 100644 src/main/resources/lib/net/pusuo/patchca-0.5.0.jar
create mode 100644 src/main/resources/local.properties
create mode 100644 src/main/resources/log4j.properties
create mode 100644 src/main/resources/mail_tpl/sym_verifycode.ftl
create mode 100644 src/main/resources/mail_tpl/sym_weekly.ftl
create mode 100644 src/main/resources/repository.json
create mode 100644 src/main/resources/robots.txt
create mode 100644 src/main/resources/scss/_common.scss
create mode 100644 src/main/resources/scss/_variables.scss
create mode 100644 src/main/resources/scss/base.scss
create mode 100644 src/main/resources/scss/error.scss
create mode 100644 src/main/resources/scss/home.scss
create mode 100644 src/main/resources/scss/index.scss
create mode 100644 src/main/resources/scss/mobile-base.scss
create mode 100644 src/main/resources/scss/responsive.scss
create mode 100644 src/main/resources/skins/classic/activity/1A0001.ftl
create mode 100644 src/main/resources/skins/classic/activity/character.ftl
create mode 100644 src/main/resources/skins/classic/activity/eating-snake.ftl
create mode 100644 src/main/resources/skins/classic/activity/gobang.ftl
create mode 100644 src/main/resources/skins/classic/admin/ad.ftl
create mode 100644 src/main/resources/skins/classic/admin/add-article.ftl
create mode 100644 src/main/resources/skins/classic/admin/add-domain.ftl
create mode 100644 src/main/resources/skins/classic/admin/add-reserved-word.ftl
create mode 100644 src/main/resources/skins/classic/admin/add-tag.ftl
create mode 100644 src/main/resources/skins/classic/admin/add-user.ftl
create mode 100644 src/main/resources/skins/classic/admin/article.ftl
create mode 100644 src/main/resources/skins/classic/admin/articles.ftl
create mode 100644 src/main/resources/skins/classic/admin/auditlog.ftl
create mode 100644 src/main/resources/skins/classic/admin/breezemoon.ftl
create mode 100644 src/main/resources/skins/classic/admin/breezemoons.ftl
create mode 100644 src/main/resources/skins/classic/admin/comment.ftl
create mode 100644 src/main/resources/skins/classic/admin/comments.ftl
create mode 100644 src/main/resources/skins/classic/admin/domain.ftl
create mode 100644 src/main/resources/skins/classic/admin/domains.ftl
create mode 100644 src/main/resources/skins/classic/admin/error.ftl
create mode 100644 src/main/resources/skins/classic/admin/index.ftl
create mode 100644 src/main/resources/skins/classic/admin/invitecode.ftl
create mode 100644 src/main/resources/skins/classic/admin/invitecodes.ftl
create mode 100644 src/main/resources/skins/classic/admin/macro-admin.ftl
create mode 100644 src/main/resources/skins/classic/admin/misc.ftl
create mode 100644 src/main/resources/skins/classic/admin/reports.ftl
create mode 100644 src/main/resources/skins/classic/admin/reserved-word.ftl
create mode 100644 src/main/resources/skins/classic/admin/reserved-words.ftl
create mode 100644 src/main/resources/skins/classic/admin/role-permissions.ftl
create mode 100644 src/main/resources/skins/classic/admin/roles.ftl
create mode 100644 src/main/resources/skins/classic/admin/tag.ftl
create mode 100644 src/main/resources/skins/classic/admin/tags.ftl
create mode 100644 src/main/resources/skins/classic/admin/user.ftl
create mode 100644 src/main/resources/skins/classic/admin/users.ftl
create mode 100644 src/main/resources/skins/classic/article.ftl
create mode 100644 src/main/resources/skins/classic/breezemoon.ftl
create mode 100644 src/main/resources/skins/classic/charge-point.ftl
create mode 100644 src/main/resources/skins/classic/chat-room.ftl
create mode 100644 src/main/resources/skins/classic/city.ftl
create mode 100644 src/main/resources/skins/classic/common/comment.ftl
create mode 100644 src/main/resources/skins/classic/common/domains.ftl
create mode 100644 src/main/resources/skins/classic/common/list-item.ftl
create mode 100644 src/main/resources/skins/classic/common/person-info.ftl
create mode 100644 src/main/resources/skins/classic/common/ranking.ftl
create mode 100644 src/main/resources/skins/classic/common/title-icon.ftl
create mode 100644 src/main/resources/skins/classic/domain-articles.ftl
create mode 100644 src/main/resources/skins/classic/domains.ftl
create mode 100644 src/main/resources/skins/classic/error/401.ftl
create mode 100644 src/main/resources/skins/classic/error/403.ftl
create mode 100644 src/main/resources/skins/classic/error/404.ftl
create mode 100644 src/main/resources/skins/classic/error/500.ftl
create mode 100644 src/main/resources/skins/classic/error/custom.ftl
create mode 100644 src/main/resources/skins/classic/footer.ftl
create mode 100644 src/main/resources/skins/classic/forward.ftl
create mode 100644 src/main/resources/skins/classic/header.ftl
create mode 100644 src/main/resources/skins/classic/home/activities.ftl
create mode 100644 src/main/resources/skins/classic/home/breezemoons.ftl
create mode 100644 src/main/resources/skins/classic/home/comments.ftl
create mode 100644 src/main/resources/skins/classic/home/followers.ftl
create mode 100644 src/main/resources/skins/classic/home/following-articles.ftl
create mode 100644 src/main/resources/skins/classic/home/following-tags.ftl
create mode 100644 src/main/resources/skins/classic/home/following-users.ftl
create mode 100644 src/main/resources/skins/classic/home/home-side.ftl
create mode 100644 src/main/resources/skins/classic/home/home.ftl
create mode 100644 src/main/resources/skins/classic/home/macro-home.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/at.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/broadcast.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/commented.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/following.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/macro-notifications.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/point.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/reply.ftl
create mode 100644 src/main/resources/skins/classic/home/notifications/sys-announce.ftl
create mode 100644 src/main/resources/skins/classic/home/points.ftl
create mode 100644 src/main/resources/skins/classic/home/post.ftl
create mode 100644 src/main/resources/skins/classic/home/pre-post.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/account.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/avatar.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/data.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/function.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/help.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/i18n.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/invite.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/macro-settings.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/point.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/privacy.ftl
create mode 100644 src/main/resources/skins/classic/home/settings/profile.ftl
create mode 100644 src/main/resources/skins/classic/home/watching-articles.ftl
create mode 100644 src/main/resources/skins/classic/hot.ftl
create mode 100644 src/main/resources/skins/classic/index.ftl
create mode 100644 src/main/resources/skins/classic/macro-head.ftl
create mode 100644 src/main/resources/skins/classic/macro-list.ftl
create mode 100644 src/main/resources/skins/classic/macro-pagination-query.ftl
create mode 100644 src/main/resources/skins/classic/macro-pagination.ftl
create mode 100644 src/main/resources/skins/classic/other/kill-browser.ftl
create mode 100644 src/main/resources/skins/classic/perfect.ftl
create mode 100644 src/main/resources/skins/classic/qna.ftl
create mode 100644 src/main/resources/skins/classic/recent.ftl
create mode 100644 src/main/resources/skins/classic/search-articles.ftl
create mode 100644 src/main/resources/skins/classic/side.ftl
create mode 100644 src/main/resources/skins/classic/skin.properties
create mode 100644 src/main/resources/skins/classic/statistic.ftl
create mode 100644 src/main/resources/skins/classic/tag-articles.ftl
create mode 100644 src/main/resources/skins/classic/tags.ftl
create mode 100644 src/main/resources/skins/classic/top/balance.ftl
create mode 100644 src/main/resources/skins/classic/top/checkin.ftl
create mode 100644 src/main/resources/skins/classic/top/consumption.ftl
create mode 100644 src/main/resources/skins/classic/top/link.ftl
create mode 100644 src/main/resources/skins/classic/top/macro-top.ftl
create mode 100644 src/main/resources/skins/classic/verify/forget-pwd.ftl
create mode 100644 src/main/resources/skins/classic/verify/guide.ftl
create mode 100644 src/main/resources/skins/classic/verify/login.ftl
create mode 100644 src/main/resources/skins/classic/verify/register.ftl
create mode 100644 src/main/resources/skins/classic/verify/register2.ftl
create mode 100644 src/main/resources/skins/classic/verify/reset-pwd.ftl
create mode 100644 src/main/resources/skins/classic/watch.ftl
create mode 100644 src/main/resources/skins/mobile/activity/1A0001.ftl
create mode 100644 src/main/resources/skins/mobile/activity/character.ftl
create mode 100644 src/main/resources/skins/mobile/admin/ad.ftl
create mode 100644 src/main/resources/skins/mobile/admin/add-article.ftl
create mode 100644 src/main/resources/skins/mobile/admin/add-domain.ftl
create mode 100644 src/main/resources/skins/mobile/admin/add-reserved-word.ftl
create mode 100644 src/main/resources/skins/mobile/admin/add-tag.ftl
create mode 100644 src/main/resources/skins/mobile/admin/add-user.ftl
create mode 100644 src/main/resources/skins/mobile/admin/article.ftl
create mode 100644 src/main/resources/skins/mobile/admin/articles.ftl
create mode 100644 src/main/resources/skins/mobile/admin/auditlog.ftl
create mode 100644 src/main/resources/skins/mobile/admin/breezemoon.ftl
create mode 100644 src/main/resources/skins/mobile/admin/breezemoons.ftl
create mode 100644 src/main/resources/skins/mobile/admin/comment.ftl
create mode 100644 src/main/resources/skins/mobile/admin/comments.ftl
create mode 100644 src/main/resources/skins/mobile/admin/domain.ftl
create mode 100644 src/main/resources/skins/mobile/admin/domains.ftl
create mode 100644 src/main/resources/skins/mobile/admin/error.ftl
create mode 100644 src/main/resources/skins/mobile/admin/index.ftl
create mode 100644 src/main/resources/skins/mobile/admin/invitecode.ftl
create mode 100644 src/main/resources/skins/mobile/admin/invitecodes.ftl
create mode 100644 src/main/resources/skins/mobile/admin/macro-admin.ftl
create mode 100644 src/main/resources/skins/mobile/admin/misc.ftl
create mode 100644 src/main/resources/skins/mobile/admin/reports.ftl
create mode 100644 src/main/resources/skins/mobile/admin/reserved-word.ftl
create mode 100644 src/main/resources/skins/mobile/admin/reserved-words.ftl
create mode 100644 src/main/resources/skins/mobile/admin/role-permissions.ftl
create mode 100644 src/main/resources/skins/mobile/admin/roles.ftl
create mode 100644 src/main/resources/skins/mobile/admin/tag.ftl
create mode 100644 src/main/resources/skins/mobile/admin/tags.ftl
create mode 100644 src/main/resources/skins/mobile/admin/user.ftl
create mode 100644 src/main/resources/skins/mobile/admin/users.ftl
create mode 100644 src/main/resources/skins/mobile/article.ftl
create mode 100644 src/main/resources/skins/mobile/breezemoon.ftl
create mode 100644 src/main/resources/skins/mobile/charge-point.ftl
create mode 100644 src/main/resources/skins/mobile/chat-room.ftl
create mode 100644 src/main/resources/skins/mobile/city.ftl
create mode 100644 src/main/resources/skins/mobile/common/comment.ftl
create mode 100644 src/main/resources/skins/mobile/common/person-info.ftl
create mode 100644 src/main/resources/skins/mobile/common/ranking.ftl
create mode 100644 src/main/resources/skins/mobile/common/sub-nav.ftl
create mode 100644 src/main/resources/skins/mobile/common/title-icon.ftl
create mode 100644 src/main/resources/skins/mobile/domain-articles.ftl
create mode 100644 src/main/resources/skins/mobile/domains.ftl
create mode 100644 src/main/resources/skins/mobile/error/401.ftl
create mode 100644 src/main/resources/skins/mobile/error/403.ftl
create mode 100644 src/main/resources/skins/mobile/error/404.ftl
create mode 100644 src/main/resources/skins/mobile/error/500.ftl
create mode 100644 src/main/resources/skins/mobile/error/custom.ftl
create mode 100644 src/main/resources/skins/mobile/footer.ftl
create mode 100644 src/main/resources/skins/mobile/forward.ftl
create mode 100644 src/main/resources/skins/mobile/header.ftl
create mode 100644 src/main/resources/skins/mobile/home/activities.ftl
create mode 100644 src/main/resources/skins/mobile/home/breezemoons.ftl
create mode 100644 src/main/resources/skins/mobile/home/comments.ftl
create mode 100644 src/main/resources/skins/mobile/home/followers.ftl
create mode 100644 src/main/resources/skins/mobile/home/following-articles.ftl
create mode 100644 src/main/resources/skins/mobile/home/following-tags.ftl
create mode 100644 src/main/resources/skins/mobile/home/following-users.ftl
create mode 100644 src/main/resources/skins/mobile/home/home-side.ftl
create mode 100644 src/main/resources/skins/mobile/home/home.ftl
create mode 100644 src/main/resources/skins/mobile/home/macro-home.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/at.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/broadcast.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/commented.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/following.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/macro-notifications.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/point.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/reply.ftl
create mode 100644 src/main/resources/skins/mobile/home/notifications/sys-announce.ftl
create mode 100644 src/main/resources/skins/mobile/home/points.ftl
create mode 100644 src/main/resources/skins/mobile/home/post.ftl
create mode 100644 src/main/resources/skins/mobile/home/pre-post.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/account.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/avatar.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/data.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/function.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/help.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/i18n.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/invite.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/macro-settings.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/point.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/privacy.ftl
create mode 100644 src/main/resources/skins/mobile/home/settings/profile.ftl
create mode 100644 src/main/resources/skins/mobile/home/watching-articles.ftl
create mode 100644 src/main/resources/skins/mobile/hot.ftl
create mode 100644 src/main/resources/skins/mobile/index.ftl
create mode 100644 src/main/resources/skins/mobile/macro-head.ftl
create mode 100644 src/main/resources/skins/mobile/macro-list.ftl
create mode 100644 src/main/resources/skins/mobile/macro-pagination-query.ftl
create mode 100644 src/main/resources/skins/mobile/macro-pagination.ftl
create mode 100644 src/main/resources/skins/mobile/other/kill-browser.ftl
create mode 100644 src/main/resources/skins/mobile/perfect.ftl
create mode 100644 src/main/resources/skins/mobile/qna.ftl
create mode 100644 src/main/resources/skins/mobile/recent.ftl
create mode 100644 src/main/resources/skins/mobile/side.ftl
create mode 100644 src/main/resources/skins/mobile/skin.properties
create mode 100644 src/main/resources/skins/mobile/statistic.ftl
create mode 100644 src/main/resources/skins/mobile/tag-articles.ftl
create mode 100644 src/main/resources/skins/mobile/tags.ftl
create mode 100644 src/main/resources/skins/mobile/top/balance.ftl
create mode 100644 src/main/resources/skins/mobile/top/checkin.ftl
create mode 100644 src/main/resources/skins/mobile/top/consumption.ftl
create mode 100644 src/main/resources/skins/mobile/top/link.ftl
create mode 100644 src/main/resources/skins/mobile/top/macro-top.ftl
create mode 100644 src/main/resources/skins/mobile/verify/forget-pwd.ftl
create mode 100644 src/main/resources/skins/mobile/verify/guide.ftl
create mode 100644 src/main/resources/skins/mobile/verify/login.ftl
create mode 100644 src/main/resources/skins/mobile/verify/register.ftl
create mode 100644 src/main/resources/skins/mobile/verify/register2.ftl
create mode 100644 src/main/resources/skins/mobile/verify/reset-pwd.ftl
create mode 100644 src/main/resources/skins/mobile/watch.ftl
create mode 100644 src/main/resources/static-resources.xml
create mode 100644 src/main/resources/symphony.properties
create mode 100644 src/test/java/org/b3log/symphony/RepositoryJSONGen.java
create mode 100644 src/test/java/org/b3log/symphony/util/ElasticsearchTestCase.java
create mode 100644 src/test/java/org/b3log/symphony/util/PanguTestCase.java
create mode 100644 src/test/java/org/b3log/symphony/util/RedditScoreTest.java
create mode 100644 src/test/java/org/b3log/symphony/util/XSSTestCase.java
create mode 100644 src/test/resources/latke.properties
create mode 100644 src/test/resources/log4j.properties
create mode 100644 src/test/resources/markdown_syntax.text
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..aabf456e5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: java
+jdk:
+ - openjdk8
+
+install:
+ - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pci
diff --git a/CHANGE_LOGS.html b/CHANGE_LOGS.html
new file mode 100644
index 000000000..9d01dd8ca
--- /dev/null
+++ b/CHANGE_LOGS.html
@@ -0,0 +1,594 @@
+
+
+
+
+
+ Sym Change Logs
+
+
+
+
+
+
+Release 3.6.0 - Nov 12, 2019
+
+ 959 Markdown HTTP 渲染引擎切换为 Lute enhancement
+ 969 Vditor 升级至 1.8.7,支持 devtool enhancement
+ 974 脱离 Servlet 规范 enhancement
+ 963 RSS 暴露“机要”内容 bug
+ 966 限制上传文件大小失效问题 bug
+ 968 移动端登录注册入口问题 bug
+ 970 User-Agent XSS 漏洞 bug
+ 971 思绪修复 bug
+ 973 Code Span 快链解析问题 bug
+
+Release 3.5.1 - Aug 19, 2019
+
+ 927 可设置帖子在列表中是否展示 feature
+ 935 支持图表 feature
+ 923 不把管理员排除在第三方统计外 enhancement
+ 941 升级 Vditor enhancement
+ 942 改进标签校验规则 enhancement
+ 943 没有相关帖子时用热议帖子填充 enhancement
+ 946 标签图标使用绝对路径 enhancement
+ 949 升级 vditor 至 1.5.12 enhancement
+ 954 Markdown 支持向 GFM 靠拢 enhancement
+ 934 数据对象字段更新优化 development
+ 936 优化签到实现 development
+ 937 升级 MySQL 驱动 development
+ 940 删除历史表情兼容 development
+ 939 本地邮件渠道 SSL 配置问题 bug
+ 944 艾特用户自动完成失效 bug
+
+Release 3.5.0 - May 17, 2019
+
+ 903 发帖、更新帖是否通知关注者开关 enhancement
+ 906 调整默认上传目录路径 enhancement
+ 907 “感谢加入”系统通知已读置位 enhancement
+ 908 vditor 升级到 1.3.3 enhancement
+ 910 设置页面页脚太高 enhancement
+ 914 Markdown 支持 align 属性 enhancement
+ 912 Upgrade tar to version 4.4.2 or later development
+ 915 细化启动日志 development
+ 919 重构查询 select 字段 development
+ 900 Error page loop /error/404 问题 bug
+ 901 帖子需要过滤大部分控制字符 bug
+ 904 仅楼主可见回帖不发通知给帖子关注者 bug
+ 909 Docker 启动验证码报错 bug
+ 913 自动获取图片上传创建目录问题 bug
+ 916 签到判断竞态条件问题 bug
+
+Release 3.4.8 - Mar 26, 2019
+
+ 898 支持内嵌 Jetty 容器启动 feature
+ 879 停用账号需重置密码 enhancement
+ 883 移除小薇 QQ 群推送 enhancement
+ 884 移除 IPFS、图灵机器人等特性 enhancement
+ 888 保留词改为违禁词 enhancement
+ 889 Vditor 升级到 0.3.0 enhancement
+ 890 表情按照设置顺序进行排列 enhancement
+ 893 vditor 升级到 1.1.10,支持甘特图、流程图、时序图、任务列表、代码复制等 enhancement
+ 894 Docker 镜像改进 enhancement
+ 895 用户名可包含横线 - enhancement
+ 896 改善头像质量 enhancement
+ 877 黑客派相关硬编码改造 development
+ 880 sym.props 配置统一 development
+ 882 简化本地邮件渠道实现 development
+ 885 latke.props 调整 development
+ 891 修复 SendCloud 邮件发送问题 bug
+ 892 Markdown 渲染问题 bug
+ 897 采纳仅楼主可见的回答泄露 bug
+
+Release 3.4.7 - Feb 22, 2019
+
+ 865 迁移到新编辑器 Vditor feature
+ 846 Markdown 支持 <details> 标签 enhancement
+ 849 优化编辑历史对比 enhancement
+ 850 推送百度搜索引擎判断 enhancement
+ 854 移除极验签到 enhancement
+ 861 更新小贴士 enhancement
+ 863 增大回帖内容长度限制 enhancement
+ 864 文件名脱敏 enhancement
+ 866 文件上传性能优化 enhancement
+ 872 更新帖子通知改进 enhancement
+ 873 支持滑稽表情 enhancement
+ 847 重构参与榜单开关常量值 development
+ 852 ORM 层接口优化 development
+ 853 重构当前用户会话上下文 development
+ 874 改进初始化判断机制 development
+ 857 任意用户登录漏洞 bug
+ 858 自动获取图片上传路径问题 bug
+ 859 数据导出问题 bug
+ 860 个性签名 XSS 漏洞 bug
+ 862 删除数据溢出问题 bug
+ 867 发文 tag 显示不全 bug
+
+Release 3.4.6 - Jan 10, 2019
+
+ 818 侧边栏加入清风明月发布入口 enhancement
+ 820 30 天未登录的用户不发关注发帖通知 enhancement
+ 821 帖子和回帖加入感谢计数字段 enhancement
+ 824 删除帖子时一并删除语音文件 enhancement
+ 825 优化相关帖子算法 enhancement
+ 826 优化清理未使用标签 enhancement
+ 827 修复不正确参数导致的异常 enhancement
+ 829 更新 Markdown 教程 enhancement
+ 830 通知标题转义问题 enhancement
+ 835 小黑屋重命名为机要 enhancement
+ 838 Forward 页加入确认按钮 enhancement
+ 839 移除 marked enhancement
+ 841 优化 Markdown 渲染 enhancement
+ 842 发布后 5 分钟内修改内容不产生历史版本 enhancement
+ 828 重构请求取参 development
+ 843 重构模板渲染构造器 development
+ 822 缩略图获取问题 bug
+ 832 更新帖子标题转义 bug
+ 833 删除保留词问题 bug
+
+Release 3.4.5 - Dec 7, 2018
+
+ 808 清风明月 UA 头处理 enhancement
+ 809 清风明月每条独立链接 enhancement
+ 811 发帖时标签改为非必填 enhancement
+ 812 清理未使用标签改进 enhancement
+ 813 回帖分页 SEO enhancement
+ 815 封禁回帖后不显示历史修订 enhancement
+ 816 定时任务重构 development
+ 817 重构控制器层 development
+ 814 生成语音摘要导致连接泄露 bug
+
+Release 3.4.4 - Nov 22, 2018
+
+ 786 管理功能操作日志 feature
+ 803 支持 webm 视频播放 enhancement
+ 804 更新帖子、回帖时艾特改进 enhancement
+ 807 校验 forward 页面来源 enhancement
+
+Release 3.4.3 - Nov 8, 2018
+
+ 774 自定义首页跳转 feature
+ 788 根据 serverScheme 自动配置 ws 协议 enhancement
+ 791 小薇语音预览更新 enhancement
+ 793 优化首页最新帖子列表查询 enhancement
+ 794 优化帖子列表查询 enhancement
+ 792 使用统一线程池 development
+ 787 IE 图片渲染问题 bug
+
+Release 3.4.2 - Oct 24, 2018
+
+ 782 支持 Live Photo feature
+ 756 积分转账加入备注 enhancement
+ 773 链接查询优化 enhancement
+ 778 标签自动完成进行过滤 enhancement
+ 781 从环境变量指定 sym.props 路径 enhancement
+ 783 重写文件上传本地服务器 enhancement
+ 784 不允许匿名浏览时的 SEO enhancement
+ 785 优化七牛云图片样式 enhancement
+ 772 重构获取登录用户 development
+ 777 升级不安全的前端库 development
+ 775 帖子快链问题 bug
+
+Release 3.4.1 - Sep 27, 2018
+
+ 747 初始化时管理员头像使用默认头像 enhancement
+ 748 清风明月快链处理 enhancement
+ 749 首页横向滚动条问题 enhancement
+ 759 增强验证码 enhancement
+ 770 通知相关 URL 规范化 enhancement
+ 771 重写模板渲染机制 enhancement
+ 751 删除 JDBC 连接池配置项 development
+ 753 升级 MySQL 驱动 development
+ 767 使用 HikariCP 作为数据库连接池 development
+ 769 重写会话管理 development
+ 745 侧栏近期热议泄露匿名帖作者 bug
+ 752 管理删帖时遗漏关联删除 bug
+ 765 编辑器乱码 bug
+
+Release 3.4.0 - Sep 6, 2018
+
+ 732 标签侧栏广告位 feature
+ 733 页脚可配置备案号 feature
+ 737 可按邮箱域名配置邮件发送渠道 feature
+ 735 可配置关闭跳转页展现 enhancement
+ 736 验证码改进 enhancement
+ 738 可配置 markdown 渲染超时时间 enhancement
+ 739 Algolia 索引调整 enhancement
+ 741 更实时的在线状态标识 enhancement
+ 740 清风明月更新漏洞 bug
+
+Release 3.3.0 - Aug 29, 2018
+
+ 686 链接榜单 feature
+ 706 支持停用账号 feature
+ 710 更新用户名 feature
+ 715 帖子可关闭回帖 feature
+ 718 注册邮件后缀白名单 enhancement
+ 721 聊天室加入时间 enhancement
+ 722 版权相关修改 enhancement
+ 723 RSS 改为全文输出 enhancement
+ 724 支持 MATLAB 语法高亮 enhancement
+ 731 后台更新回帖不生成版本 enhancement
+ 705 持久化帖子首图地址 development
+ 707 查看回帖的回复和原回复时泄漏仅楼主可见内容 bug
+ 708 重置密码漏洞 bug
+ 719 连续签到积分显示问题 bug
+ 728 腾讯视频分享被过滤的问题 bug
+ 729 用户主页 XSS 漏洞 bug
+
+Release 3.2.0 - Aug 2, 2018
+
+ 682 回帖可选择仅楼主和自己可见 feature
+ 694 帖子浏览数可配置为使用排重机制计数 feature
+ 677 问答帖列表入口 enhancement
+ 679 举报处理优化 enhancement
+ 688 聊天室重构 enhancement
+ 690 清风明月记录地理位置 enhancement
+ 693 消息提示重构 enhancement
+ 696 清风明月图片处理优化 enhancement
+ 699 减少验证码字母 enhancement
+ 687 链接访问排重机制 development
+ 691 取消关注 HTTP 请求方法调整 development
+ 697 重构系统机器人账号 development
+ 701 删除 B3 构思相关实现 development
+ 703 工具类重构 development
+ 684 修复视频播放器渲染问题 bug
+ 692 带锚点的帖子快链生成问题 bug
+
+Release 3.1.0 - Jul 12, 2018
+
+ 174 RSS 订阅 feature
+ 537 周邮件推送帖可后台设置 feature
+ 638 举报系统 feature
+ 664 删除通知 feature
+ 658 DomainCache 可能引发并发的问题 enhancement
+ 663 删除角色 enhancement
+ 666 后台管理界面优化 enhancement
+ 672 问答帖悬赏积分必须大于 20 enhancement
+ 673 数学公式渲染改进 enhancement
+ 642 超过 30 天的帖子置顶无效 bug
+ 659 Input 标签注入问题 bug
+
+Release 3.0.0 - Jun 13, 2018
+
+Release 2.8.0 - May 25, 2018
+
+Release 2.7.0 - May 4, 2018
+
+Release 2.6.0 - Apr 10, 2018
+
+Release 2.5.0 - Mar 14, 2018
+
+Release 2.4.0 - Feb 7, 2018
+
+Release 2.3.0 - Dec 13, 2017
+
+Release 2.2.0 - Nov 1, 2017
+
+Release 2.1.0 - May 18, 2017
+
+Release 2.0.0 - Mar 7, 2017
+
+Release 1.9.0 - Jan 12, 2017
+
+Release 1.8.0 - Dec 26, 2016
+
+Release 1.7.0 - Dec 7, 2016
+
+Release 1.6.0 - Oct 19, 2016
+
+Release 1.5.0 - Aug 19, 2016
+
+Release 1.4.0 - Jul 29, 2016
+
+Release 1.3.0 - Oct 15, 2015
+
+Release 1.0.0 - Jan 18, 2015
+
+Release 0.2.5 - Mar 28, 2014
+
+Release 0.2.1 - Aug 25, 2013
+
+
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..70bc96cb1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM maven:3-jdk-8-alpine as MVN_BUILD
+
+WORKDIR /opt/sym/
+ADD . /tmp
+RUN cd /tmp && mvn package -DskipTests -Pci -q && mv target/symphony/* /opt/sym/ \
+&& cp -f /tmp/src/main/resources/docker/* /opt/sym/
+
+FROM openjdk:8-alpine
+LABEL maintainer="Liang Ding"
+
+WORKDIR /opt/sym/
+COPY --from=MVN_BUILD /opt/sym/ /opt/sym/
+RUN apk add --no-cache ca-certificates tzdata ttf-dejavu
+
+ENV TZ=Asia/Shanghai
+EXPOSE 8080
+
+ENTRYPOINT [ "java", "-cp", "lib/*:.", "org.b3log.symphony.Server" ]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..bae94e189
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+ .
\ No newline at end of file
diff --git a/README.md b/README.md
index 785d5b744..c8c4dbd46 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,15 @@
-# symphony
-🎶 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。
+* [一款用 Java 实现的现代化社区(论坛 /BBS/ 社交网络 / 博客)平台](https://hacpai.com/article/1570193280819)
+
+以下截图来自 Sym 商业版。
+
+![index](https://user-images.githubusercontent.com/970828/68215287-edade380-0019-11ea-8af3-877f6cec716e.png)
+
+![list](https://user-images.githubusercontent.com/970828/61682145-08966980-ad43-11e9-9d90-b70c194e3d8b.png)
+
+![article](https://user-images.githubusercontent.com/970828/68215922-08348c80-001b-11ea-9163-3c2fd2ccfef0.png)
+
+![post](https://user-images.githubusercontent.com/970828/61682148-092f0000-ad43-11e9-8de3-46e35ec4b474.png)
+
+![pc home](https://user-images.githubusercontent.com/970828/68214389-5300d500-0018-11ea-9c58-9edb9d066353.png)
+
+![theme](https://user-images.githubusercontent.com/970828/68213511-af62f500-0016-11ea-8c82-1c20d23907e1.png)
diff --git a/THIRD_PARTY_LICENSE b/THIRD_PARTY_LICENSE
new file mode 100644
index 000000000..c5dd835e3
--- /dev/null
+++ b/THIRD_PARTY_LICENSE
@@ -0,0 +1,46 @@
+# Backend
+
+autolink: MIT
+commons*: APL
+hikaricp: APL
+emoji-java: MIT
+flexmark*: BSD
+freemarker: APL
+gson: APL
+javassist: APL
+jodd*: BSD
+jsoup: MIT
+latke*: APL
+log4j: APL
+mysql-connector-java: GPL
+ok*: APL
+patchca: GPL
+qiniu-java-sdk: MIT
+slf4j*: MIT
+UserAgentUtils: BSD
+
+# Frontend
+
+aplayer: SATA
+algolia*: MIT
+echarts: BSD
+emojify.js: MIT
+codemirror: MIT
+highlight.js: BSD
+jquery: MIT
+jquery.fileupload: MIT
+isotope: GPL/C
+jquery.bowknot: APL
+jquery.hotkeys: MIT
+jquery.linkify: MIT
+jquery.pjax: MIT
+jquery.qrcode: MIT
+nprogress: MIT
+SoundRecorder: CC BY-NC-SA/C
+MathJax: APL
+md5: BSD
+reconnecting-websocket: MIT
+to-markdown: MIT
+ua-parser: MIT
+diff2html: MIT
+flowchart: MIT
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 000000000..6b7182e33
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,117 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * @file frontend tool.
+ *
+ * @author Liyuan Li
+ * @author Liang Ding
+ * @version 1.8.0.2, Mar 17, 2019
+ */
+
+'use strict'
+
+var gulp = require('gulp')
+var concat = require('gulp-concat')
+var cleanCSS = require('gulp-clean-css')
+var uglify = require('gulp-uglify')
+var sass = require('gulp-sass')
+var rename = require('gulp-rename')
+var del = require('del')
+
+function sassProcess () {
+ return gulp.src('./src/main/resources/scss/*.scss').
+ pipe(sass({outputStyle: 'compressed', includePaths: ['node_modules']}).
+ on('error', sass.logError)).
+ pipe(gulp.dest('./src/main/resources/css'))
+}
+
+function sassProcessWatch () {
+ gulp.watch('./src/main/resources/scss/*.scss', sassProcess)
+}
+
+gulp.task('watch', gulp.series(sassProcessWatch))
+
+function cleanProcess () {
+ return del(['./src/main/resources/js/*.min.js'])
+}
+
+function minArticleCSS () {
+ // min article css
+ return gulp.src([
+ './src/main/resources/js/lib/diff2html/diff2html.min.css']).
+ pipe(cleanCSS()).
+ pipe(concat('article.min.css')).
+ pipe(gulp.dest('./src/main/resources/js/lib/compress/'))
+}
+
+function minJS () {
+ // min js
+ return gulp.src('./src/main/resources/js/*.js').
+ pipe(uglify()).
+ pipe(rename({suffix: '.min'})).
+ pipe(gulp.dest('./src/main/resources/js/'))
+}
+
+function minUpload () {
+ // concat js
+ var jsJqueryUpload = [
+ './src/main/resources/js/lib/jquery/file-upload-9.10.1/vendor/jquery.ui.widget.js',
+ './src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.iframe-transport.js',
+ './src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload.js',
+ './src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload-process.js',
+ './src/main/resources/js/lib/jquery/file-upload-9.10.1/jquery.fileupload-validate.js']
+ return gulp.src(jsJqueryUpload).
+ pipe(uglify()).
+ pipe(concat('jquery.fileupload.min.js')).
+ pipe(gulp.dest('./src/main/resources/js/lib/jquery/file-upload-9.10.1/'))
+}
+
+function minLibs () {
+ var jsCommonLib = [
+ './src/main/resources/js/lib/jquery/jquery-3.1.0.min.js',
+ './src/main/resources/js/lib/md5.js',
+ './src/main/resources/js/lib/reconnecting-websocket.min.js',
+ './src/main/resources/js/lib/jquery/jquery.bowknot.min.js',
+ './src/main/resources/js/lib/ua-parser.min.js',
+ './src/main/resources/js/lib/jquery/jquery.hotkeys.js',
+ './src/main/resources/js/lib/jquery/jquery.pjax.js',
+ './src/main/resources/js/lib/nprogress/nprogress.js']
+ return gulp.src(jsCommonLib).
+ pipe(uglify()).
+ pipe(concat('libs.min.js')).
+ pipe(gulp.dest('./src/main/resources/js/lib/compress/'))
+}
+
+function minArticleLibs () {
+ var jsArticleLib = [
+ './src/main/resources/js/lib/sound-recorder/SoundRecorder.js',
+ './src/main/resources/js/lib/jquery/jquery.qrcode.min.js',
+ './src/main/resources/js/lib/aplayer/APlayer.min.js',
+ './src/main/resources/js/lib/diff2html/diff2html.min.js',
+ './src/main/resources/js/lib/diff2html/diff2html-ui.min.js',
+ './src/main/resources/js/lib/diff2html/diff.min.js']
+ return gulp.src(jsArticleLib).
+ pipe(uglify()).
+ pipe(concat('article-libs.min.js')).
+ pipe(gulp.dest('./src/main/resources/js/lib/compress/'))
+}
+
+gulp.task('default',
+ gulp.series(cleanProcess, sassProcess,
+ gulp.parallel(minJS, minUpload, minLibs),
+ gulp.parallel(minArticleCSS, minArticleLibs)))
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..f486a1301
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6179 @@
+{
+ "name": "Symphony",
+ "version": "3.6.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@braintree/sanitize-url": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz",
+ "integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg=="
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz",
+ "integrity": "sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.1",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz",
+ "integrity": "sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz",
+ "integrity": "sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.1",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@types/events": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
+ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
+ "dev": true
+ },
+ "@types/glob": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
+ "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
+ "dev": true,
+ "requires": {
+ "@types/events": "*",
+ "@types/minimatch": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/minimatch": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "12.6.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz",
+ "integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==",
+ "dev": true
+ },
+ "abab": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
+ "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg=="
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
+ "abcjs": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/abcjs/-/abcjs-5.10.2.tgz",
+ "integrity": "sha512-b8QFjz2dXEXprFFeL1r6+a+CInmB+iPM204Vzyb/Qw9AqmxOjmgDgEaC0ateUoXO4iRvOTGWLeHasjK2PzOKvg==",
+ "requires": {
+ "midi": "git+https://github.com/paulrosen/MIDI.js.git#abcjs"
+ }
+ },
+ "acorn": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+ "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
+ },
+ "acorn-globals": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz",
+ "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==",
+ "requires": {
+ "acorn": "^6.0.1",
+ "acorn-walk": "^6.0.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
+ "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA=="
+ }
+ }
+ },
+ "acorn-walk": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
+ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
+ },
+ "ajv": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz",
+ "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==",
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+ "dev": true
+ },
+ "ansi-colors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
+ "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==",
+ "dev": true,
+ "requires": {
+ "ansi-wrap": "^0.1.0"
+ }
+ },
+ "ansi-gray": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
+ "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
+ "dev": true,
+ "requires": {
+ "ansi-wrap": "0.1.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "ansi-wrap": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
+ "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
+ "dev": true
+ },
+ "anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "dev": true,
+ "requires": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
+ }
+ },
+ "append-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz",
+ "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=",
+ "dev": true,
+ "requires": {
+ "buffer-equal": "^1.0.0"
+ }
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
+ "dev": true
+ },
+ "archy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
+ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
+ "dev": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
+ "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "dev": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+ "dev": true
+ },
+ "arr-filter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz",
+ "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=",
+ "dev": true,
+ "requires": {
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "dev": true
+ },
+ "arr-map": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz",
+ "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=",
+ "dev": true,
+ "requires": {
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+ "dev": true
+ },
+ "array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
+ "dev": true
+ },
+ "array-equal": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
+ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM="
+ },
+ "array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+ "dev": true
+ },
+ "array-initial": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz",
+ "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=",
+ "dev": true,
+ "requires": {
+ "array-slice": "^1.0.0",
+ "is-number": "^4.0.0"
+ },
+ "dependencies": {
+ "is-number": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+ "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+ "dev": true
+ }
+ }
+ },
+ "array-last": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz",
+ "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==",
+ "dev": true,
+ "requires": {
+ "is-number": "^4.0.0"
+ },
+ "dependencies": {
+ "is-number": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+ "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+ "dev": true
+ }
+ }
+ },
+ "array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true
+ },
+ "array-sort": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz",
+ "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==",
+ "dev": true,
+ "requires": {
+ "default-compare": "^1.0.0",
+ "get-value": "^2.0.6",
+ "kind-of": "^5.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true
+ },
+ "array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+ "dev": true
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ },
+ "assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+ "dev": true
+ },
+ "async-done": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz",
+ "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.2",
+ "process-nextick-args": "^2.0.0",
+ "stream-exhaust": "^1.0.1"
+ }
+ },
+ "async-each": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
+ "dev": true
+ },
+ "async-foreach": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+ "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+ "dev": true
+ },
+ "async-limiter": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
+ },
+ "async-settle": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz",
+ "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=",
+ "dev": true,
+ "requires": {
+ "async-done": "^1.2.2"
+ }
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+ },
+ "atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "dev": true
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
+ },
+ "aws4": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
+ },
+ "bach": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
+ "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=",
+ "dev": true,
+ "requires": {
+ "arr-filter": "^1.1.1",
+ "arr-flatten": "^1.0.1",
+ "arr-map": "^2.0.0",
+ "array-each": "^1.0.0",
+ "array-initial": "^1.0.0",
+ "array-last": "^1.1.1",
+ "async-done": "^1.2.2",
+ "async-settle": "^1.0.0",
+ "now-and-later": "^2.0.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "dev": true,
+ "requires": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
+ "dev": true
+ },
+ "block-stream": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+ "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+ "dev": true,
+ "requires": {
+ "inherits": "~2.0.0"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browser-process-hrtime": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
+ "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw=="
+ },
+ "buffer-equal": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz",
+ "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=",
+ "dev": true
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+ },
+ "cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "dev": true,
+ "requires": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ }
+ },
+ "camel-case": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
+ "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
+ "requires": {
+ "no-case": "^2.2.0",
+ "upper-case": "^1.1.1"
+ }
+ },
+ "camelcase": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+ "dev": true
+ },
+ "camelcase-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^2.0.0",
+ "map-obj": "^1.0.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+ "dev": true
+ }
+ }
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chokidar": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
+ "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==",
+ "dev": true,
+ "requires": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "fsevents": "^1.2.7",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ }
+ },
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
+ }
+ },
+ "class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "clean-css": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
+ "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
+ "requires": {
+ "source-map": "~0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+ "dev": true
+ },
+ "clone-buffer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz",
+ "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=",
+ "dev": true
+ },
+ "clone-stats": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz",
+ "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=",
+ "dev": true
+ },
+ "cloneable-readable": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz",
+ "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "process-nextick-args": "^2.0.0",
+ "readable-stream": "^2.3.5"
+ },
+ "dependencies": {
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+ "dev": true
+ }
+ }
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+ "dev": true
+ },
+ "collection-map": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
+ "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=",
+ "dev": true,
+ "requires": {
+ "arr-map": "^2.0.2",
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "dev": true,
+ "requires": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "dev": true
+ },
+ "combined-stream": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+ "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
+ "component-emitter": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "concat-with-sourcemaps": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz",
+ "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+ "dev": true
+ },
+ "convert-source-map": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+ "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+ "dev": true
+ },
+ "copy-props": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz",
+ "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==",
+ "dev": true,
+ "requires": {
+ "each-props": "^1.3.0",
+ "is-plain-object": "^2.0.1"
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "cross-spawn": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+ "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "which": "^1.2.9"
+ }
+ },
+ "crypto-random-string": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.0.1.tgz",
+ "integrity": "sha512-dUL0cJ4PBLanJGJQBHQUkvZ3C4q13MXzl54oRqAIiJGiNkOZ4JDwkg/SBo7daGghzlJv16yW1p/4lIQukmbedA==",
+ "requires": {
+ "type-fest": "^0.5.2"
+ }
+ },
+ "css-b64-images": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/css-b64-images/-/css-b64-images-0.2.5.tgz",
+ "integrity": "sha1-QgBdgyBLK0pdk7axpWRBM7WSegI="
+ },
+ "cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
+ },
+ "cssstyle": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz",
+ "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==",
+ "requires": {
+ "cssom": "0.3.x"
+ }
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "dev": true,
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "d": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+ "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+ "dev": true,
+ "requires": {
+ "es5-ext": "^0.10.50",
+ "type": "^1.0.1"
+ }
+ },
+ "d3": {
+ "version": "5.14.1",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-5.14.1.tgz",
+ "integrity": "sha512-x5VpyP/IS8DP8zePZtcoXmILmM3+YeF4mbvhH6bHICQPPuPYgfcxuGsZZBTh8+e2w9L/5myX76IVxWcPdizNxg==",
+ "requires": {
+ "d3-array": "1",
+ "d3-axis": "1",
+ "d3-brush": "1",
+ "d3-chord": "1",
+ "d3-collection": "1",
+ "d3-color": "1",
+ "d3-contour": "1",
+ "d3-dispatch": "1",
+ "d3-drag": "1",
+ "d3-dsv": "1",
+ "d3-ease": "1",
+ "d3-fetch": "1",
+ "d3-force": "1",
+ "d3-format": "1",
+ "d3-geo": "1",
+ "d3-hierarchy": "1",
+ "d3-interpolate": "1",
+ "d3-path": "1",
+ "d3-polygon": "1",
+ "d3-quadtree": "1",
+ "d3-random": "1",
+ "d3-scale": "2",
+ "d3-scale-chromatic": "1",
+ "d3-selection": "1",
+ "d3-shape": "1",
+ "d3-time": "1",
+ "d3-time-format": "2",
+ "d3-timer": "1",
+ "d3-transition": "1",
+ "d3-voronoi": "1",
+ "d3-zoom": "1"
+ }
+ },
+ "d3-array": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
+ "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
+ },
+ "d3-axis": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
+ "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
+ },
+ "d3-brush": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.4.tgz",
+ "integrity": "sha512-DRcFXGcVZiJF644i78m/HM8P0U19hWYRFcAnacOVGvpOpzkqGm3H0EAoAH6jwyY/lqfH49d+5vz2TgznP/zlMA==",
+ "requires": {
+ "d3-dispatch": "1",
+ "d3-drag": "1",
+ "d3-interpolate": "1",
+ "d3-selection": "1",
+ "d3-transition": "1"
+ }
+ },
+ "d3-chord": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
+ "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
+ "requires": {
+ "d3-array": "1",
+ "d3-path": "1"
+ }
+ },
+ "d3-collection": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
+ "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
+ },
+ "d3-color": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz",
+ "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg=="
+ },
+ "d3-contour": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
+ "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
+ "requires": {
+ "d3-array": "^1.1.1"
+ }
+ },
+ "d3-dispatch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
+ "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
+ },
+ "d3-drag": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz",
+ "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
+ "requires": {
+ "d3-dispatch": "1",
+ "d3-selection": "1"
+ }
+ },
+ "d3-dsv": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz",
+ "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==",
+ "requires": {
+ "commander": "2",
+ "iconv-lite": "0.4",
+ "rw": "1"
+ }
+ },
+ "d3-ease": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz",
+ "integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ=="
+ },
+ "d3-fetch": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
+ "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
+ "requires": {
+ "d3-dsv": "1"
+ }
+ },
+ "d3-force": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
+ "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==",
+ "requires": {
+ "d3-collection": "1",
+ "d3-dispatch": "1",
+ "d3-quadtree": "1",
+ "d3-timer": "1"
+ }
+ },
+ "d3-format": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.2.tgz",
+ "integrity": "sha512-gco1Ih54PgMsyIXgttLxEhNy/mXxq8+rLnCb5shQk+P5TsiySrwWU5gpB4zen626J4LIwBxHvDChyA8qDm57ww=="
+ },
+ "d3-geo": {
+ "version": "1.11.9",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.9.tgz",
+ "integrity": "sha512-9edcH6J3s/Aa3KJITWqFJbyB/8q3mMlA9Fi7z6yy+FAYMnRaxmC7jBhUnsINxVWD14GmqX3DK8uk7nV6/Ekt4A==",
+ "requires": {
+ "d3-array": "1"
+ }
+ },
+ "d3-hierarchy": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
+ "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ=="
+ },
+ "d3-interpolate": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.3.tgz",
+ "integrity": "sha512-wTsi4AqnC2raZ3Q9eqFxiZGUf5r6YiEdi23vXjjKSWXFYLCQNUtBVMk6uk2tg4cOY6YrjRdmSmI/Mf0ze1zPzQ==",
+ "requires": {
+ "d3-color": "1"
+ }
+ },
+ "d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+ },
+ "d3-polygon": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz",
+ "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ=="
+ },
+ "d3-quadtree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz",
+ "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA=="
+ },
+ "d3-random": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
+ "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
+ },
+ "d3-scale": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
+ "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
+ "requires": {
+ "d3-array": "^1.2.0",
+ "d3-collection": "1",
+ "d3-format": "1",
+ "d3-interpolate": "1",
+ "d3-time": "1",
+ "d3-time-format": "2"
+ }
+ },
+ "d3-scale-chromatic": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
+ "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
+ "requires": {
+ "d3-color": "1",
+ "d3-interpolate": "1"
+ }
+ },
+ "d3-selection": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.1.tgz",
+ "integrity": "sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA=="
+ },
+ "d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "requires": {
+ "d3-path": "1"
+ }
+ },
+ "d3-time": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
+ "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
+ },
+ "d3-time-format": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.2.tgz",
+ "integrity": "sha512-pweL2Ri2wqMY+wlW/wpkl8T3CUzKAha8S9nmiQlMABab8r5MJN0PD1V4YyRNVaKQfeh4Z0+VO70TLw6ESVOYzw==",
+ "requires": {
+ "d3-time": "1"
+ }
+ },
+ "d3-timer": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
+ "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
+ },
+ "d3-transition": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz",
+ "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==",
+ "requires": {
+ "d3-color": "1",
+ "d3-dispatch": "1",
+ "d3-ease": "1",
+ "d3-interpolate": "1",
+ "d3-selection": "^1.1.0",
+ "d3-timer": "1"
+ }
+ },
+ "d3-voronoi": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
+ "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
+ },
+ "d3-zoom": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz",
+ "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==",
+ "requires": {
+ "d3-dispatch": "1",
+ "d3-drag": "1",
+ "d3-interpolate": "1",
+ "d3-selection": "1",
+ "d3-transition": "1"
+ }
+ },
+ "dagre": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz",
+ "integrity": "sha512-Dj0csFDrWYKdavwROb9FccHfTC4fJbyF/oJdL9LNZJ8WUvl968P6PAKEriGqfbdArVJEmmfA+UyumgWEwcHU6A==",
+ "requires": {
+ "graphlib": "^2.1.7",
+ "lodash": "^4.17.4"
+ }
+ },
+ "dagre-d3": {
+ "version": "github:dagrejs/dagre-d3#e1a00e5cb518f5d2304a35647e024f31d178e55b",
+ "from": "github:dagrejs/dagre-d3",
+ "requires": {
+ "d3": "^5.12",
+ "dagre": "^0.8.4",
+ "graphlib": "^2.1.7",
+ "lodash": "^4.17.15"
+ },
+ "dependencies": {
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ }
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "data-urls": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz",
+ "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==",
+ "requires": {
+ "abab": "^2.0.0",
+ "whatwg-mimetype": "^2.2.0",
+ "whatwg-url": "^7.0.0"
+ },
+ "dependencies": {
+ "whatwg-url": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
+ "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ }
+ }
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
+ },
+ "default-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz",
+ "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^5.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "default-resolution": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz",
+ "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "dependencies": {
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "del": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/del/-/del-5.0.0.tgz",
+ "integrity": "sha512-TfU3nUY0WDIhN18eq+pgpbLY9AfL5RfiE9czKaTSolc6aK7qASXfDErvYgjV1UqCR4sNXDoxO0/idPmhDUt2Sg==",
+ "dev": true,
+ "requires": {
+ "globby": "^10.0.0",
+ "is-path-cwd": "^2.0.0",
+ "is-path-in-cwd": "^2.0.0",
+ "p-map": "^2.0.0",
+ "rimraf": "^2.6.3"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+ "dev": true
+ },
+ "detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+ "dev": true
+ },
+ "diff-match-patch": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz",
+ "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg=="
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ },
+ "dependencies": {
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true
+ }
+ }
+ },
+ "domexception": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
+ "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
+ "requires": {
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "each-props": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz",
+ "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.1",
+ "object.defaults": "^1.1.0"
+ }
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "echarts": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-4.5.0.tgz",
+ "integrity": "sha512-q9M0errodeX/786uPifro76x0elbrUQkbSHh235QzbkaASuvP9AQoMErhGBno4iC/yq6kFDLqgmm3XCPWQGLzA==",
+ "requires": {
+ "zrender": "4.1.2"
+ }
+ },
+ "end-of-stream": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+ "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+ "dev": true,
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es5-ext": {
+ "version": "0.10.50",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz",
+ "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==",
+ "dev": true,
+ "requires": {
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "~3.1.1",
+ "next-tick": "^1.0.0"
+ }
+ },
+ "es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "es6-symbol": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
+ "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
+ "es6-weak-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+ "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "^0.10.46",
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "escaper": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz",
+ "integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ=="
+ },
+ "escodegen": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz",
+ "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==",
+ "requires": {
+ "esprima": "^3.1.3",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true
+ }
+ }
+ },
+ "esprima": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
+ "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
+ },
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
+ },
+ "expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "dev": true,
+ "requires": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "dev": true,
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "requires": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+ },
+ "fancy-log": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz",
+ "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==",
+ "dev": true,
+ "requires": {
+ "ansi-gray": "^0.1.1",
+ "color-support": "^1.1.3",
+ "parse-node-version": "^1.0.0",
+ "time-stamp": "^1.0.0"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+ },
+ "fast-glob": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.0.4.tgz",
+ "integrity": "sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.1",
+ "@nodelib/fs.walk": "^1.2.1",
+ "glob-parent": "^5.0.0",
+ "is-glob": "^4.0.1",
+ "merge2": "^1.2.3",
+ "micromatch": "^4.0.2"
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
+ },
+ "fastq": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz",
+ "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.0"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "dev": true,
+ "requires": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "findup-sync": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
+ "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==",
+ "dev": true,
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
+ }
+ },
+ "fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ }
+ },
+ "flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "dev": true
+ },
+ "flush-write-stream": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
+ }
+ },
+ "for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+ "dev": true
+ },
+ "for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.1"
+ }
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+ },
+ "form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "dev": true,
+ "requires": {
+ "map-cache": "^0.2.2"
+ }
+ },
+ "fs-mkdirp-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz",
+ "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "through2": "^2.0.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+ "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "nan": "^2.12.1",
+ "node-pre-gyp": "^0.12.0"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "debug": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "fs-minipass": {
+ "version": "1.2.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore-walk": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minipass": {
+ "version": "2.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.2.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "needle": {
+ "version": "2.3.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "debug": "^4.1.0",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-pre-gyp": {
+ "version": "0.12.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.1",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.2.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4"
+ }
+ },
+ "nopt": {
+ "version": "4.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "npm-packlist": {
+ "version": "1.4.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "rc": {
+ "version": "1.2.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "semver": {
+ "version": "5.7.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "tar": {
+ "version": "4.4.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.3.4",
+ "minizlib": "^1.1.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "yallist": {
+ "version": "3.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "fstream": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ }
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "dev": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "gaze": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+ "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+ "dev": true,
+ "requires": {
+ "globule": "^1.0.0"
+ }
+ },
+ "get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "dev": true
+ },
+ "get-stdin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+ "dev": true
+ },
+ "get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+ "dev": true
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz",
+ "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "glob-stream": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
+ "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=",
+ "dev": true,
+ "requires": {
+ "extend": "^3.0.0",
+ "glob": "^7.1.1",
+ "glob-parent": "^3.1.0",
+ "is-negated-glob": "^1.0.0",
+ "ordered-read-streams": "^1.0.0",
+ "pumpify": "^1.3.5",
+ "readable-stream": "^2.1.5",
+ "remove-trailing-separator": "^1.0.1",
+ "to-absolute-glob": "^2.0.0",
+ "unique-stream": "^2.0.2"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ }
+ }
+ },
+ "glob-watcher": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz",
+ "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==",
+ "dev": true,
+ "requires": {
+ "anymatch": "^2.0.0",
+ "async-done": "^1.2.0",
+ "chokidar": "^2.0.0",
+ "is-negated-glob": "^1.0.0",
+ "just-debounce": "^1.0.0",
+ "object.defaults": "^1.1.0"
+ }
+ },
+ "global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "requires": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ }
+ },
+ "global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ }
+ },
+ "globby": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
+ "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
+ "dev": true,
+ "requires": {
+ "@types/glob": "^7.1.1",
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.0.3",
+ "glob": "^7.1.3",
+ "ignore": "^5.1.1",
+ "merge2": "^1.2.3",
+ "slash": "^3.0.0"
+ }
+ },
+ "globule": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+ "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+ "dev": true,
+ "requires": {
+ "glob": "~7.1.1",
+ "lodash": "~4.17.10",
+ "minimatch": "~3.0.2"
+ }
+ },
+ "glogg": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz",
+ "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==",
+ "dev": true,
+ "requires": {
+ "sparkles": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.15",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
+ "dev": true
+ },
+ "graphlib": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz",
+ "integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==",
+ "requires": {
+ "lodash": "^4.17.5"
+ }
+ },
+ "gulp": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
+ "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==",
+ "dev": true,
+ "requires": {
+ "glob-watcher": "^5.0.3",
+ "gulp-cli": "^2.2.0",
+ "undertaker": "^1.2.1",
+ "vinyl-fs": "^3.0.0"
+ },
+ "dependencies": {
+ "gulp-cli": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz",
+ "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^1.0.1",
+ "archy": "^1.0.0",
+ "array-sort": "^1.0.0",
+ "color-support": "^1.1.3",
+ "concat-stream": "^1.6.0",
+ "copy-props": "^2.0.1",
+ "fancy-log": "^1.3.2",
+ "gulplog": "^1.0.0",
+ "interpret": "^1.1.0",
+ "isobject": "^3.0.1",
+ "liftoff": "^3.1.0",
+ "matchdep": "^2.0.0",
+ "mute-stdout": "^1.0.0",
+ "pretty-hrtime": "^1.0.0",
+ "replace-homedir": "^1.0.0",
+ "semver-greatest-satisfied-range": "^1.1.0",
+ "v8flags": "^3.0.1",
+ "yargs": "^7.1.0"
+ }
+ }
+ }
+ },
+ "gulp-clean-css": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-4.2.0.tgz",
+ "integrity": "sha512-r4zQsSOAK2UYUL/ipkAVCTRg/2CLZ2A+oPVORopBximRksJ6qy3EX1KGrIWT4ZrHxz3Hlobb1yyJtqiut7DNjA==",
+ "dev": true,
+ "requires": {
+ "clean-css": "4.2.1",
+ "plugin-error": "1.0.1",
+ "through2": "3.0.1",
+ "vinyl-sourcemaps-apply": "0.2.1"
+ },
+ "dependencies": {
+ "through2": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
+ "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "2 || 3"
+ }
+ }
+ }
+ },
+ "gulp-concat": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz",
+ "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=",
+ "dev": true,
+ "requires": {
+ "concat-with-sourcemaps": "^1.0.0",
+ "through2": "^2.0.0",
+ "vinyl": "^2.0.0"
+ }
+ },
+ "gulp-rename": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz",
+ "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==",
+ "dev": true
+ },
+ "gulp-sass": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-4.0.2.tgz",
+ "integrity": "sha512-q8psj4+aDrblJMMtRxihNBdovfzGrXJp1l4JU0Sz4b/Mhsi2DPrKFYCGDwjIWRENs04ELVHxdOJQ7Vs98OFohg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.3.0",
+ "lodash.clonedeep": "^4.3.2",
+ "node-sass": "^4.8.3",
+ "plugin-error": "^1.0.1",
+ "replace-ext": "^1.0.0",
+ "strip-ansi": "^4.0.0",
+ "through2": "^2.0.0",
+ "vinyl-sourcemaps-apply": "^0.2.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "gulp-uglify": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gulp-uglify/-/gulp-uglify-3.0.2.tgz",
+ "integrity": "sha512-gk1dhB74AkV2kzqPMQBLA3jPoIAPd/nlNzP2XMDSG8XZrqnlCiDGAqC+rZOumzFvB5zOphlFh6yr3lgcAb/OOg==",
+ "dev": true,
+ "requires": {
+ "array-each": "^1.0.1",
+ "extend-shallow": "^3.0.2",
+ "gulplog": "^1.0.0",
+ "has-gulplog": "^0.1.0",
+ "isobject": "^3.0.1",
+ "make-error-cause": "^1.1.1",
+ "safe-buffer": "^5.1.2",
+ "through2": "^2.0.0",
+ "uglify-js": "^3.0.5",
+ "vinyl-sourcemaps-apply": "^0.2.0"
+ }
+ },
+ "gulplog": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz",
+ "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=",
+ "dev": true,
+ "requires": {
+ "glogg": "^1.0.0"
+ }
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
+ },
+ "har-validator": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "requires": {
+ "ajv": "^6.5.5",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "has-gulplog": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz",
+ "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=",
+ "dev": true,
+ "requires": {
+ "sparkles": "^1.0.0"
+ }
+ },
+ "has-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+ "dev": true
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+ "dev": true
+ },
+ "has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+ "dev": true,
+ "requires": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "dependencies": {
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
+ },
+ "highlight.js": {
+ "version": "9.16.2",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.16.2.tgz",
+ "integrity": "sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw=="
+ },
+ "homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "requires": {
+ "parse-passwd": "^1.0.0"
+ }
+ },
+ "hosted-git-info": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+ "dev": true
+ },
+ "html-encoding-sniffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
+ "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
+ "requires": {
+ "whatwg-encoding": "^1.0.1"
+ }
+ },
+ "html-minifier": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
+ "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
+ "requires": {
+ "camel-case": "^3.0.0",
+ "clean-css": "^4.2.1",
+ "commander": "^2.19.0",
+ "he": "^1.2.0",
+ "param-case": "^2.1.1",
+ "relateurl": "^0.2.7",
+ "uglify-js": "^3.5.1"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.2.tgz",
+ "integrity": "sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==",
+ "dev": true
+ },
+ "in-publish": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+ "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+ "dev": true
+ },
+ "indent-string": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "dev": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+ "dev": true
+ },
+ "interpret": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
+ "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
+ "dev": true
+ },
+ "invert-kv": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+ "dev": true
+ },
+ "is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "requires": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^1.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-finite": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-negated-glob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
+ "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-path-cwd": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+ "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
+ "dev": true
+ },
+ "is-path-in-cwd": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+ "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
+ "dev": true,
+ "requires": {
+ "is-path-inside": "^2.1.0"
+ }
+ },
+ "is-path-inside": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+ "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
+ "dev": true,
+ "requires": {
+ "path-is-inside": "^1.0.2"
+ }
+ },
+ "is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "is-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk="
+ },
+ "is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "requires": {
+ "is-unc-path": "^1.0.0"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+ },
+ "is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "requires": {
+ "unc-path-regex": "^0.1.2"
+ }
+ },
+ "is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+ "dev": true
+ },
+ "is-valid-glob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
+ "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=",
+ "dev": true
+ },
+ "is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+ },
+ "js-base64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
+ "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
+ "dev": true
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
+ },
+ "jsdom": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
+ "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
+ "requires": {
+ "abab": "^2.0.0",
+ "acorn": "^5.5.3",
+ "acorn-globals": "^4.1.0",
+ "array-equal": "^1.0.0",
+ "cssom": ">= 0.3.2 < 0.4.0",
+ "cssstyle": "^1.0.0",
+ "data-urls": "^1.0.0",
+ "domexception": "^1.0.1",
+ "escodegen": "^1.9.1",
+ "html-encoding-sniffer": "^1.0.2",
+ "left-pad": "^1.3.0",
+ "nwsapi": "^2.0.7",
+ "parse5": "4.0.0",
+ "pn": "^1.1.0",
+ "request": "^2.87.0",
+ "request-promise-native": "^1.0.5",
+ "sax": "^1.2.4",
+ "symbol-tree": "^3.2.2",
+ "tough-cookie": "^2.3.4",
+ "w3c-hr-time": "^1.0.1",
+ "webidl-conversions": "^4.0.2",
+ "whatwg-encoding": "^1.0.3",
+ "whatwg-mimetype": "^2.1.0",
+ "whatwg-url": "^6.4.1",
+ "ws": "^5.2.0",
+ "xml-name-validator": "^3.0.0"
+ }
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "just-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz",
+ "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=",
+ "dev": true
+ },
+ "katex": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.11.1.tgz",
+ "integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
+ "requires": {
+ "commander": "^2.19.0"
+ }
+ },
+ "kind-of": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "dev": true
+ },
+ "last-run": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz",
+ "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=",
+ "dev": true,
+ "requires": {
+ "default-resolution": "^2.0.0",
+ "es6-weak-map": "^2.0.1"
+ }
+ },
+ "lazystream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
+ "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.0.5"
+ }
+ },
+ "lcid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+ "dev": true,
+ "requires": {
+ "invert-kv": "^1.0.0"
+ }
+ },
+ "lead": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz",
+ "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=",
+ "dev": true,
+ "requires": {
+ "flush-write-stream": "^1.0.2"
+ }
+ },
+ "left-pad": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
+ "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA=="
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "liftoff": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
+ "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==",
+ "dev": true,
+ "requires": {
+ "extend": "^3.0.0",
+ "findup-sync": "^3.0.0",
+ "fined": "^1.0.1",
+ "flagged-respawn": "^1.0.0",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.0",
+ "rechoir": "^0.6.2",
+ "resolve": "^1.1.7"
+ }
+ },
+ "load-json-file": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "strip-bom": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "lodash": {
+ "version": "4.17.14",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
+ "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
+ },
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+ "dev": true
+ },
+ "lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
+ },
+ "loud-rejection": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+ "dev": true,
+ "requires": {
+ "currently-unhandled": "^0.4.1",
+ "signal-exit": "^3.0.0"
+ }
+ },
+ "lower-case": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+ "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
+ },
+ "lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "dev": true,
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "make-error": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
+ "dev": true
+ },
+ "make-error-cause": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-1.2.2.tgz",
+ "integrity": "sha1-3wOI/NCzeBbf8KX7gQiTl3fcvJ0=",
+ "dev": true,
+ "requires": {
+ "make-error": "^1.2.0"
+ }
+ },
+ "make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
+ "map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+ "dev": true
+ },
+ "map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+ "dev": true
+ },
+ "map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+ "dev": true,
+ "requires": {
+ "object-visit": "^1.0.0"
+ }
+ },
+ "matchdep": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
+ "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=",
+ "dev": true,
+ "requires": {
+ "findup-sync": "^2.0.0",
+ "micromatch": "^3.0.4",
+ "resolve": "^1.4.0",
+ "stack-trace": "0.0.10"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "findup-sync": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+ "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+ "dev": true,
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ }
+ },
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
+ }
+ },
+ "meow": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+ "dev": true,
+ "requires": {
+ "camelcase-keys": "^2.0.0",
+ "decamelize": "^1.1.2",
+ "loud-rejection": "^1.0.0",
+ "map-obj": "^1.0.1",
+ "minimist": "^1.1.3",
+ "normalize-package-data": "^2.3.4",
+ "object-assign": "^4.0.1",
+ "read-pkg-up": "^1.0.1",
+ "redent": "^1.0.0",
+ "trim-newlines": "^1.0.0"
+ }
+ },
+ "merge2": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz",
+ "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==",
+ "dev": true
+ },
+ "mermaid": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.4.2.tgz",
+ "integrity": "sha512-vYSCP2u4XkOnjliWz/QIYwvzF/znQAq22vWJJ3YV40SnwV2JQyHblnwwNYXCprkXw7XfwBKDpSNaJ3HP4WfnZw==",
+ "requires": {
+ "@braintree/sanitize-url": "^3.1.0",
+ "crypto-random-string": "^3.0.1",
+ "d3": "^5.7.0",
+ "dagre": "^0.8.4",
+ "dagre-d3": "github:dagrejs/dagre-d3",
+ "graphlib": "^2.1.7",
+ "he": "^1.2.0",
+ "lodash": "^4.17.11",
+ "minify": "^4.1.1",
+ "moment-mini": "^2.22.1",
+ "prettier": "^1.18.2",
+ "scope-css": "^1.2.1"
+ }
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "midi": {
+ "version": "git+https://github.com/paulrosen/MIDI.js.git#e593ffef81a0350f99448e3ab8111957145ff6b2",
+ "from": "git+https://github.com/paulrosen/MIDI.js.git#abcjs"
+ },
+ "mime-db": {
+ "version": "1.37.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
+ "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
+ },
+ "mime-types": {
+ "version": "2.1.21",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
+ "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
+ "requires": {
+ "mime-db": "~1.37.0"
+ }
+ },
+ "minify": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/minify/-/minify-4.1.3.tgz",
+ "integrity": "sha512-ykuscavxivSmVpcCzsXmsVTukWYLUUtPhHj0w2ILvHDGqC+hsuTCihBn9+PJBd58JNvWTNg9132J9nrrI2anzA==",
+ "requires": {
+ "clean-css": "^4.1.6",
+ "css-b64-images": "~0.2.5",
+ "debug": "^4.1.0",
+ "html-minifier": "^4.0.0",
+ "terser": "^4.0.0",
+ "try-catch": "^2.0.0",
+ "try-to-catch": "^1.0.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+ "dev": true
+ },
+ "mixin-deep": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+ "dev": true,
+ "requires": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ }
+ }
+ },
+ "moment-mini": {
+ "version": "2.22.1",
+ "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.22.1.tgz",
+ "integrity": "sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw=="
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "mute-stdout": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz",
+ "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==",
+ "dev": true
+ },
+ "nan": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
+ "dev": true,
+ "optional": true
+ },
+ "nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ }
+ },
+ "next-tick": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
+ "dev": true
+ },
+ "no-case": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
+ "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
+ "requires": {
+ "lower-case": "^1.1.1"
+ }
+ },
+ "node-gyp": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+ "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+ "dev": true,
+ "requires": {
+ "fstream": "^1.0.0",
+ "glob": "^7.0.3",
+ "graceful-fs": "^4.1.2",
+ "mkdirp": "^0.5.0",
+ "nopt": "2 || 3",
+ "npmlog": "0 || 1 || 2 || 3 || 4",
+ "osenv": "0",
+ "request": "^2.87.0",
+ "rimraf": "2",
+ "semver": "~5.3.0",
+ "tar": "^2.0.0",
+ "which": "1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+ "dev": true
+ },
+ "tar": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
+ "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
+ "dev": true,
+ "requires": {
+ "block-stream": "*",
+ "fstream": "^1.0.12",
+ "inherits": "2"
+ }
+ }
+ }
+ },
+ "node-sass": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
+ "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
+ "dev": true,
+ "requires": {
+ "async-foreach": "^0.1.3",
+ "chalk": "^1.1.1",
+ "cross-spawn": "^3.0.0",
+ "gaze": "^1.0.0",
+ "get-stdin": "^4.0.1",
+ "glob": "^7.0.3",
+ "in-publish": "^2.0.0",
+ "lodash": "^4.17.11",
+ "meow": "^3.7.0",
+ "mkdirp": "^0.5.1",
+ "nan": "^2.13.2",
+ "node-gyp": "^3.8.0",
+ "npmlog": "^4.0.0",
+ "request": "^2.88.0",
+ "sass-graph": "^2.2.4",
+ "stdout-stream": "^1.4.0",
+ "true-case-path": "^1.0.2"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "nan": {
+ "version": "2.13.2",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
+ "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
+ }
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "dev": true,
+ "requires": {
+ "remove-trailing-separator": "^1.0.1"
+ }
+ },
+ "now-and-later": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz",
+ "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.2"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "dev": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true
+ },
+ "nwsapi": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
+ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ=="
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
+ },
+ "object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+ "dev": true,
+ "requires": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ }
+ },
+ "object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
+ "dev": true,
+ "requires": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
+ "dev": true,
+ "requires": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+ "dev": true,
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "object.reduce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz",
+ "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=",
+ "dev": true,
+ "requires": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ }
+ },
+ "ordered-read-streams": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz",
+ "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+ "dev": true
+ },
+ "os-locale": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+ "dev": true,
+ "requires": {
+ "lcid": "^1.0.0"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "dev": true
+ },
+ "param-case": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+ "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+ "requires": {
+ "no-case": "^2.2.0"
+ }
+ },
+ "parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
+ "dev": true,
+ "requires": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ }
+ },
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "parse-node-version": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+ "dev": true
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+ "dev": true
+ },
+ "parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
+ },
+ "pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+ "dev": true
+ },
+ "path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "dev": true,
+ "requires": {
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
+ "dev": true,
+ "requires": {
+ "path-root-regex": "^0.1.0"
+ }
+ },
+ "path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=",
+ "dev": true
+ },
+ "path-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ }
+ }
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+ },
+ "picomatch": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz",
+ "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==",
+ "dev": true
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+ "dev": true
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "dev": true,
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
+ "plugin-error": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
+ "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^1.0.1",
+ "arr-diff": "^4.0.0",
+ "arr-union": "^3.1.0",
+ "extend-shallow": "^3.0.2"
+ }
+ },
+ "pn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
+ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA=="
+ },
+ "posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
+ },
+ "prettier": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
+ "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew=="
+ },
+ "pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
+ "dev": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+ "dev": true
+ },
+ "psl": {
+ "version": "1.1.31",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
+ "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
+ },
+ "pump": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+ "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "pumpify": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
+ "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
+ "dev": true,
+ "requires": {
+ "duplexify": "^3.6.0",
+ "inherits": "^2.0.3",
+ "pump": "^2.0.0"
+ }
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+ },
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "read-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+ "dev": true,
+ "requires": {
+ "load-json-file": "^1.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^1.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+ "dev": true,
+ "requires": {
+ "find-up": "^1.0.0",
+ "read-pkg": "^1.0.0"
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ },
+ "dependencies": {
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+ "dev": true
+ }
+ }
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "dev": true,
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "dev": true,
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ }
+ }
+ },
+ "rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+ "dev": true,
+ "requires": {
+ "resolve": "^1.1.6"
+ }
+ },
+ "redent": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+ "dev": true,
+ "requires": {
+ "indent-string": "^2.1.0",
+ "strip-indent": "^1.0.1"
+ }
+ },
+ "regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ }
+ },
+ "relateurl": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+ "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
+ },
+ "remove-bom-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz",
+ "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5",
+ "is-utf8": "^0.2.1"
+ }
+ },
+ "remove-bom-stream": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz",
+ "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=",
+ "dev": true,
+ "requires": {
+ "remove-bom-buffer": "^3.0.0",
+ "safe-buffer": "^5.1.0",
+ "through2": "^2.0.3"
+ }
+ },
+ "remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+ "dev": true
+ },
+ "repeat-element": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+ "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+ "dev": true
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
+ "replace-ext": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+ "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
+ "dev": true
+ },
+ "replace-homedir": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz",
+ "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1",
+ "is-absolute": "^1.0.0",
+ "remove-trailing-separator": "^1.1.0"
+ }
+ },
+ "request": {
+ "version": "2.88.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.0",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.4.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ }
+ },
+ "request-promise-core": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
+ "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
+ "requires": {
+ "lodash": "^4.17.15"
+ },
+ "dependencies": {
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ }
+ }
+ },
+ "request-promise-native": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz",
+ "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==",
+ "requires": {
+ "request-promise-core": "1.1.3",
+ "stealthy-require": "^1.1.1",
+ "tough-cookie": "^2.3.3"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
+ "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ }
+ },
+ "resolve-options": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz",
+ "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=",
+ "dev": true,
+ "requires": {
+ "value-or-function": "^3.0.0"
+ }
+ },
+ "resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+ "dev": true
+ },
+ "ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+ "dev": true
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-parallel": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
+ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
+ "dev": true
+ },
+ "rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "dev": true,
+ "requires": {
+ "ret": "~0.1.10"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "sass-graph": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+ "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.0",
+ "lodash": "^4.0.0",
+ "scss-tokenizer": "^0.2.3",
+ "yargs": "^7.0.0"
+ }
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
+ "scope-css": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/scope-css/-/scope-css-1.2.1.tgz",
+ "integrity": "sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==",
+ "requires": {
+ "escaper": "^2.5.3",
+ "slugify": "^1.3.1",
+ "strip-css-comments": "^3.0.0"
+ }
+ },
+ "scss-tokenizer": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+ "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+ "dev": true,
+ "requires": {
+ "js-base64": "^2.1.8",
+ "source-map": "^0.4.2"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+ "dev": true,
+ "requires": {
+ "amdefine": ">=0.0.4"
+ }
+ }
+ }
+ },
+ "semver": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+ "dev": true
+ },
+ "semver-greatest-satisfied-range": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz",
+ "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=",
+ "dev": true,
+ "requires": {
+ "sver-compat": "^1.5.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "set-value": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
+ },
+ "slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true
+ },
+ "slugify": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.6.tgz",
+ "integrity": "sha512-wA9XS475ZmGNlEnYYLPReSfuz/c3VQsEMoU43mi6OnKMCdbnFXd4/Yg7J0lBv8jkPolacMpOrWEaoYxuE1+hoQ=="
+ },
+ "snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "dev": true,
+ "requires": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "dev": true,
+ "requires": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.2.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+ "dev": true
+ },
+ "source-map-resolve": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+ "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+ "dev": true,
+ "requires": {
+ "atob": "^2.1.1",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "source-map-support": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+ "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "source-map-url": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+ "dev": true
+ },
+ "sparkles": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz",
+ "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==",
+ "dev": true
+ },
+ "spdx-correct": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+ "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+ "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz",
+ "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==",
+ "dev": true
+ },
+ "split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^3.0.0"
+ }
+ },
+ "sshpk": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
+ "dev": true
+ },
+ "static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+ "dev": true,
+ "requires": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "stdout-stream": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
+ "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "^2.0.1"
+ }
+ },
+ "stealthy-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
+ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
+ },
+ "stream-exhaust": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz",
+ "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==",
+ "dev": true
+ },
+ "stream-shift": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
+ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "dev": true,
+ "requires": {
+ "is-utf8": "^0.2.0"
+ }
+ },
+ "strip-css-comments": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-3.0.0.tgz",
+ "integrity": "sha1-elYl7/iisibPiUehElTaluE9rok=",
+ "requires": {
+ "is-regexp": "^1.0.0"
+ }
+ },
+ "strip-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+ "dev": true,
+ "requires": {
+ "get-stdin": "^4.0.1"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "sver-compat": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz",
+ "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=",
+ "dev": true,
+ "requires": {
+ "es6-iterator": "^2.0.1",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
+ },
+ "terser": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.0.tgz",
+ "integrity": "sha512-oDG16n2WKm27JO8h4y/w3iqBGAOSCtq7k8dRmrn4Wf9NouL0b2WpMHGChFGZq4nFAQy1FsNJrVQHfurXOSTmOA==",
+ "requires": {
+ "commander": "^2.20.0",
+ "source-map": "~0.6.1",
+ "source-map-support": "~0.5.12"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "through2-filter": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz",
+ "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==",
+ "dev": true,
+ "requires": {
+ "through2": "~2.0.0",
+ "xtend": "~4.0.0"
+ }
+ },
+ "time-stamp": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
+ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=",
+ "dev": true
+ },
+ "to-absolute-glob": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
+ "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=",
+ "dev": true,
+ "requires": {
+ "is-absolute": "^1.0.0",
+ "is-negated-glob": "^1.0.0"
+ }
+ },
+ "to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "dev": true,
+ "requires": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "to-through": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz",
+ "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=",
+ "dev": true,
+ "requires": {
+ "through2": "^2.0.3"
+ }
+ },
+ "tough-cookie": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+ "requires": {
+ "psl": "^1.1.24",
+ "punycode": "^1.4.1"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+ }
+ }
+ },
+ "tr46": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
+ "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "trim-newlines": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+ "dev": true
+ },
+ "true-case-path": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+ "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.2"
+ }
+ },
+ "try-catch": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-2.0.1.tgz",
+ "integrity": "sha512-LsOrmObN/2WdM+y2xG+t16vhYrQsnV8wftXIcIOWZhQcBJvKGYuamJGwnU98A7Jxs2oZNkJztXlphEOoA0DWqg=="
+ },
+ "try-to-catch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-1.1.1.tgz",
+ "integrity": "sha512-ikUlS+/BcImLhNYyIgZcEmq4byc31QpC+46/6Jm5ECWkVFhf8SM2Fp/0pMVXPX6vk45SMCwrP4Taxucne8I0VA=="
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "turndown": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/turndown/-/turndown-5.0.3.tgz",
+ "integrity": "sha512-popfGXEiedpq6F5saRIAThKxq/bbEPVFnsDnUdjaDGIre9f3/OL9Yi/yPbPcZ7RYUDpekghr666bBfZPrwNnhQ==",
+ "requires": {
+ "jsdom": "^11.9.0"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+ },
+ "type": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz",
+ "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "type-fest": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz",
+ "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw=="
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+ "dev": true
+ },
+ "uglify-js": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
+ "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
+ "requires": {
+ "commander": "~2.20.0",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+ "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
+ "dev": true
+ },
+ "undertaker": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz",
+ "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==",
+ "dev": true,
+ "requires": {
+ "arr-flatten": "^1.0.1",
+ "arr-map": "^2.0.0",
+ "bach": "^1.0.0",
+ "collection-map": "^1.0.0",
+ "es6-weak-map": "^2.0.1",
+ "last-run": "^1.1.0",
+ "object.defaults": "^1.0.0",
+ "object.reduce": "^1.0.0",
+ "undertaker-registry": "^1.0.0"
+ }
+ },
+ "undertaker-registry": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz",
+ "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=",
+ "dev": true
+ },
+ "union-value": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "dev": true,
+ "requires": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ }
+ },
+ "unique-stream": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
+ "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==",
+ "dev": true,
+ "requires": {
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "through2-filter": "^3.0.0"
+ }
+ },
+ "unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+ "dev": true,
+ "requires": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "dependencies": {
+ "has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+ "dev": true,
+ "requires": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "dependencies": {
+ "isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "dev": true,
+ "requires": {
+ "isarray": "1.0.0"
+ }
+ }
+ }
+ },
+ "has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+ "dev": true
+ }
+ }
+ },
+ "upath": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
+ "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
+ "dev": true
+ },
+ "upper-case": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
+ "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
+ },
+ "uri-js": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+ "dev": true
+ },
+ "use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+ "dev": true
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ },
+ "v8flags": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz",
+ "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "value-or-function": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz",
+ "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=",
+ "dev": true
+ },
+ "vditor": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/vditor/-/vditor-1.10.0.tgz",
+ "integrity": "sha512-KiyeTHbj24vw5oAZjd36l4MdgrgBQViLNp2f9ifUFn0VVK9TMnW25BOXFAoV5bDMleI8Z9mBiZ6v5PriSzLj7w==",
+ "requires": {
+ "abcjs": "^5.8.0",
+ "diff-match-patch": "^1.0.4",
+ "echarts": "^4.2.1",
+ "highlight.js": "^9.15.9",
+ "katex": "^0.11.0",
+ "mermaid": "^8.2.3",
+ "turndown": "^5.0.3"
+ }
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "vinyl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz",
+ "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==",
+ "dev": true,
+ "requires": {
+ "clone": "^2.1.1",
+ "clone-buffer": "^1.0.0",
+ "clone-stats": "^1.0.0",
+ "cloneable-readable": "^1.0.0",
+ "remove-trailing-separator": "^1.0.1",
+ "replace-ext": "^1.0.0"
+ }
+ },
+ "vinyl-fs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz",
+ "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==",
+ "dev": true,
+ "requires": {
+ "fs-mkdirp-stream": "^1.0.0",
+ "glob-stream": "^6.1.0",
+ "graceful-fs": "^4.0.0",
+ "is-valid-glob": "^1.0.0",
+ "lazystream": "^1.0.0",
+ "lead": "^1.0.0",
+ "object.assign": "^4.0.4",
+ "pumpify": "^1.3.5",
+ "readable-stream": "^2.3.3",
+ "remove-bom-buffer": "^3.0.0",
+ "remove-bom-stream": "^1.2.0",
+ "resolve-options": "^1.1.0",
+ "through2": "^2.0.0",
+ "to-through": "^2.0.0",
+ "value-or-function": "^3.0.0",
+ "vinyl": "^2.0.0",
+ "vinyl-sourcemap": "^1.1.0"
+ }
+ },
+ "vinyl-sourcemap": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz",
+ "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=",
+ "dev": true,
+ "requires": {
+ "append-buffer": "^1.0.2",
+ "convert-source-map": "^1.5.0",
+ "graceful-fs": "^4.1.6",
+ "normalize-path": "^2.1.1",
+ "now-and-later": "^2.0.0",
+ "remove-bom-buffer": "^3.0.0",
+ "vinyl": "^2.0.0"
+ }
+ },
+ "vinyl-sourcemaps-apply": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz",
+ "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.5.1"
+ }
+ },
+ "w3c-hr-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",
+ "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=",
+ "requires": {
+ "browser-process-hrtime": "^0.1.2"
+ }
+ },
+ "webidl-conversions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
+ },
+ "whatwg-encoding": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
+ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
+ "requires": {
+ "iconv-lite": "0.4.24"
+ }
+ },
+ "whatwg-mimetype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
+ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="
+ },
+ "whatwg-url": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
+ "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
+ "requires": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+ "dev": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
+ },
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "ws": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
+ "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
+ "requires": {
+ "async-limiter": "~1.0.0"
+ }
+ },
+ "xml-name-validator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
+ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+ "dev": true
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+ "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^3.0.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^1.4.0",
+ "read-pkg-up": "^1.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^1.0.2",
+ "which-module": "^1.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^5.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+ "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^3.0.0"
+ }
+ },
+ "zrender": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.1.2.tgz",
+ "integrity": "sha512-MJYEo1ZOVesjxYsfcGtPXnUREmh4ACMV08QZLGZ3S7D1xOd96iz3O6nf6pv5PHb5NSHkbizr7ChSIgtAGwncvA=="
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..bd763f11e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "Symphony",
+ "version": "3.6.0",
+ "description": "A modern community (forum/BBS/SNS/blog) platform written in Java. 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。",
+ "homepage": "https://sym.b3log.org",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/b3log/symphony.git"
+ },
+ "bugs": {
+ "url": "https://github.com/b3log/symphony/issues"
+ },
+ "license": "AGPLv3",
+ "private": true,
+ "author": "Daniel (http://88250.b3log.org) & Vanessa (http://vanessa.b3log.org)",
+ "scripts": {
+ "dev": "gulp watch",
+ "build": "gulp"
+ },
+ "maintainers": [
+ {
+ "name": "Daniel",
+ "email": "d@b3log.org"
+ },
+ {
+ "name": "Vanessa",
+ "email": "v@b3log.org"
+ }
+ ],
+ "devDependencies": {
+ "del": "^5.0.0",
+ "gulp": "^4.0.2",
+ "gulp-clean-css": "^4.2.0",
+ "gulp-concat": "^2.6.1",
+ "gulp-rename": "^1.4.0",
+ "gulp-sass": "^4.0.2",
+ "gulp-uglify": "^3.0.2"
+ },
+ "dependencies": {
+ "vditor": "^1.10.0"
+ }
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 000000000..eb750ecbd
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,413 @@
+
+
+
+ 4.0.0
+ org.b3log
+ symphony
+ jar
+ 3.6.0
+ Symphony
+ https://sym.b3log.org
+
+ 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。
+
+ 2012
+
+
+ 3.0.17
+
+ 1.7.28
+ 8.0.18
+ 1.12.1
+ 0.50.40
+ 7.2.23
+ 5.0.13
+ 5.1.0
+ 1.21
+ 2.3.1
+ 22.0
+ 1.2.2
+ 1.18
+ 1.6.2
+ 1.3.1
+
+ 3.0
+ 2.22.1
+ 3.2.2
+ 6.14.3
+
+ 1.8
+ 1.8
+ UTF-8
+
+
+
+ B3log
+ https://b3log.org
+
+
+
+
+ GNU Affero General Public License, Version 3
+ https://www.gnu.org/licenses/agpl-3.0.txt
+
+
+
+
+
+ d@b3log.org
+ Liang Ding
+ d@b3log.org
+ http://88250.b3log.org
+
+ Lead
+ Initial Committer
+
+ B3log
+ https://b3log.org
+ +8
+
+
+
+ v@b3log.org
+ Liyuan Li
+ v@b3log.org
+ http://vanessa.b3log.org
+
+ Committer
+
+ B3log
+ https://b3log.org
+ +8
+
+
+
+ wmainlove@gmail.com
+ Liceng Yao
+ wmainlove@gmail.com
+ http://love.b3log.org
+
+ Committer
+
+ B3log
+ https://b3log.org
+ +8
+
+
+
+ O2895205695@gmail.com
+ Zephyr Cheung
+ O2895205695@gmail.com
+ http://zephyr.b3log.org
+
+ Committer
+
+ B3log
+ https://b3log.org
+ +8
+
+
+
+
+
+ GitHub Issues
+ https://github.com/b3log/symphony/issues
+
+
+
+ https://github.com/b3log/symphony
+
+
+
+
+ org.slf4j
+ slf4j-log4j12
+ ${slf4j.version}
+
+
+
+ org.b3log
+ latke-core
+ ${latke.version}
+
+
+ com.h2database
+ h2
+
+
+
+ org.jboss
+ jboss-vfs
+
+
+
+
+
+ org.b3log
+ latke-repository-mysql
+ ${latke.version}
+
+
+
+ mysql
+ mysql-connector-java
+ ${mysql-connector-java.version}
+
+
+
+ org.testng
+ testng
+ ${testng.version}
+ test
+
+
+
+ commons-cli
+ commons-cli
+ ${commons-cli.version}
+
+
+
+ org.jsoup
+ jsoup
+ ${jsoup.version}
+
+
+
+ com.vladsch.flexmark
+ flexmark-all
+ ${flexmark.version}
+
+
+ com.vladsch.flexmark
+ flexmark-pdf-converter
+
+
+ com.vladsch.flexmark
+ flexmark-youtrack-converter
+
+
+ com.vladsch.flexmark
+ flexmark-jira-converter
+
+
+
+
+
+ com.qiniu
+ qiniu-java-sdk
+ ${qiniu.version}
+
+
+
+ org.jodd
+ jodd-http
+ ${jodd.version}
+
+
+
+ com.vdurmont
+ emoji-java
+ ${emoji-java.version}
+
+
+ org.json
+ json
+
+
+
+
+
+ eu.bitwalker
+ UserAgentUtils
+ ${user-agent-utils.version}
+
+
+
+ org.patchca
+ patchca
+ 0.5.0
+ system
+ ${project.basedir}/src/main/resources/lib/net/pusuo/patchca-0.5.0.jar
+
+
+
+ org.elasticsearch
+ elasticsearch
+ ${es.version}
+ test
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+ org.owasp.encoder
+ encoder
+ ${owasp.version}
+
+
+
+ org.yaml
+ snakeyaml
+ ${snakeyaml.version}
+
+
+
+ com.sun.mail
+ javax.mail
+ ${javax.mail.version}
+
+
+
+
+
+
+
+ src/main/resources
+
+ etc/
+ lib/
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+ false
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ ${license-maven-plugin.version}
+
+ ${basedir}
+ src/main/resources/etc/header.txt
+ false
+ true
+ true
+ true
+
+ **/src/*/java/**/*.java
+ **/src/*/resources/js/*.js
+ **/src/*/resources/css/*.css
+ **/src/*/resources/scss/*.scss
+ **/src/*/resources/*.properties
+ **/src/*/resources/skins/*/*.properties
+ **/src/*/resources/WEB-INF/*.xml
+ **/src/*/resources/*.xml
+ gulpfile.js
+ **/src/*/resources/**/*.ftl
+
+
+ **/src/main/java/**/package-info.java
+ **/src/main/java/**/Pangu.java
+ **/src/*/resources/js/lib/*.js
+ **/src/*/resources/js/*.min.js
+ **/src/*/resources/css/*.css
+
+
+ true
+
+ SLASHSTAR_STYLE
+ SLASHSTAR_STYLE
+
+ true
+
+ 2012-present
+ b3log.org
+
+ UTF-8
+
+
+
+ generate-resources
+
+ format
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ org.b3log.symphony.Server
+ true
+ lib/
+
+
+
+
+
+
+ maven-assembly-plugin
+
+
+ src/assembly/bin.xml
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
+ symphony
+
+
+
+
+ ci
+
+
+
+ dev
+
+ true
+
+
+
+ aliyun
+ http://maven.aliyun.com/nexus/content/groups/public
+
+
+
+
+
+ aliyun
+ http://maven.aliyun.com/nexus/content/groups/public
+
+
+
+
+
+
+
diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml
new file mode 100644
index 000000000..ff81593d8
--- /dev/null
+++ b/src/assembly/bin.xml
@@ -0,0 +1,32 @@
+
+
+ dir
+
+ false
+
+
+ /
+ true
+
+ ${artifact}
+
+
+
+ /lib
+ false
+
+ ${artifact}
+
+ system
+
+
+ /lib
+ false
+
+ ${artifact}
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/org/b3log/symphony/Server.java b/src/main/java/org/b3log/symphony/Server.java
new file mode 100644
index 000000000..fbd91acb8
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/Server.java
@@ -0,0 +1,244 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony;
+
+import org.apache.commons.cli.*;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.event.EventManager;
+import org.b3log.latke.http.BaseServer;
+import org.b3log.latke.http.Dispatcher;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.cache.DomainCache;
+import org.b3log.symphony.cache.TagCache;
+import org.b3log.symphony.event.*;
+import org.b3log.symphony.processor.AfterRequestHandler;
+import org.b3log.symphony.processor.BeforeRequestHandler;
+import org.b3log.symphony.processor.ErrorProcessor;
+import org.b3log.symphony.processor.channel.*;
+import org.b3log.symphony.service.CronMgmtService;
+import org.b3log.symphony.service.InitMgmtService;
+import org.b3log.symphony.util.Markdowns;
+import org.b3log.symphony.util.Symphonys;
+
+/**
+ * Server.
+ *
+ * @author Liang Ding
+ * @version 2.0.0.1, Nov 12, 2019
+ * @since 3.4.8
+ */
+public final class Server extends BaseServer {
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(Server.class);
+
+ /**
+ * Symphony version.
+ */
+ public static final String VERSION = "3.6.0";
+
+ /**
+ * Main.
+ *
+ * @param args the specified arguments
+ */
+ public static void main(final String[] args) {
+ Stopwatchs.start("Booting");
+
+ final Options options = new Options();
+ final Option listenPortOpt = Option.builder("lp").longOpt("listen_port").argName("LISTEN_PORT")
+ .hasArg().desc("listen port, default is 8080").build();
+ options.addOption(listenPortOpt);
+
+ final Option serverSchemeOpt = Option.builder("ss").longOpt("server_scheme").argName("SERVER_SCHEME")
+ .hasArg().desc("browser visit protocol, default is http").build();
+ options.addOption(serverSchemeOpt);
+
+ final Option serverHostOpt = Option.builder("sh").longOpt("server_host").argName("SERVER_HOST")
+ .hasArg().desc("browser visit domain name, default is localhost").build();
+ options.addOption(serverHostOpt);
+
+ final Option serverPortOpt = Option.builder("sp").longOpt("server_port").argName("SERVER_PORT")
+ .hasArg().desc("browser visit port, default is 8080").build();
+ options.addOption(serverPortOpt);
+
+ final Option staticServerSchemeOpt = Option.builder("sss").longOpt("static_server_scheme").argName("STATIC_SERVER_SCHEME")
+ .hasArg().desc("browser visit static resource protocol, default is http").build();
+ options.addOption(staticServerSchemeOpt);
+
+ final Option staticServerHostOpt = Option.builder("ssh").longOpt("static_server_host").argName("STATIC_SERVER_HOST")
+ .hasArg().desc("browser visit static resource domain name, default is localhost").build();
+ options.addOption(staticServerHostOpt);
+
+ final Option staticServerPortOpt = Option.builder("ssp").longOpt("static_server_port").argName("STATIC_SERVER_PORT")
+ .hasArg().desc("browser visit static resource port, default is 8080").build();
+ options.addOption(staticServerPortOpt);
+
+ final Option runtimeModeOpt = Option.builder("rm").longOpt("runtime_mode").argName("RUNTIME_MODE")
+ .hasArg().desc("runtime mode (DEVELOPMENT/PRODUCTION), default is DEVELOPMENT").build();
+ options.addOption(runtimeModeOpt);
+
+ options.addOption("h", "help", false, "print help for the command");
+
+ final HelpFormatter helpFormatter = new HelpFormatter();
+ final CommandLineParser commandLineParser = new DefaultParser();
+ CommandLine commandLine;
+
+ final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
+ final String cmdSyntax = isWindows ? "java -cp \"lib/*;.\" org.b3log.symphony.Server"
+ : "java -cp \"lib/*:.\" org.b3log.symphony.Server";
+ final String header = "\nSym 是一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。\n\n";
+ final String footer = "\n提需求或报告缺陷请到项目网站: https://github.com/b3log/symphony\n\n";
+ try {
+ commandLine = commandLineParser.parse(options, args);
+ } catch (final ParseException e) {
+ helpFormatter.printHelp(cmdSyntax, header, options, footer, true);
+
+ return;
+ }
+
+ if (commandLine.hasOption("h")) {
+ helpFormatter.printHelp(cmdSyntax, header, options, footer, true);
+
+ return;
+ }
+
+ String portArg = commandLine.getOptionValue("listen_port");
+ if (!Strings.isNumeric(portArg)) {
+ portArg = "8080";
+ }
+
+ try {
+ Latkes.setScanPath("org.b3log.symphony");
+ Latkes.init();
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Latke init failed, please configure latke.props or run with args, visit https://hacpai.com/article/1492881378588 for more details");
+
+ System.exit(-1);
+ }
+
+ String serverScheme = commandLine.getOptionValue("server_scheme");
+ if (null != serverScheme) {
+ Latkes.setLatkeProperty("serveScheme", serverScheme);
+ }
+ String serverHost = commandLine.getOptionValue("server_host");
+ if (null != serverHost) {
+ Latkes.setLatkeProperty("serverHost", serverHost);
+ }
+ String serverPort = commandLine.getOptionValue("server_port");
+ if (null != serverPort) {
+ Latkes.setLatkeProperty("serverPort", serverPort);
+ }
+ String staticServerScheme = commandLine.getOptionValue("static_server_scheme");
+ if (null != staticServerScheme) {
+ Latkes.setLatkeProperty("staticServerScheme", staticServerScheme);
+ }
+ String staticServerHost = commandLine.getOptionValue("static_server_host");
+ if (null != staticServerHost) {
+ Latkes.setLatkeProperty("staticServerHost", staticServerHost);
+ }
+ String staticServerPort = commandLine.getOptionValue("static_server_port");
+ if (null != staticServerPort) {
+ Latkes.setLatkeProperty("staticServerPort", staticServerPort);
+ }
+ String runtimeMode = commandLine.getOptionValue("runtime_mode");
+ if (null != runtimeMode) {
+ Latkes.setRuntimeMode(Latkes.RuntimeMode.valueOf(runtimeMode));
+ }
+
+ Dispatcher.startRequestHandler = new BeforeRequestHandler();
+ Dispatcher.endRequestHandler = new AfterRequestHandler();
+
+ final Latkes.RuntimeDatabase runtimeDatabase = Latkes.getRuntimeDatabase();
+ final String jdbcUsername = Latkes.getLocalProperty("jdbc.username");
+ final String jdbcURL = Latkes.getLocalProperty("jdbc.URL");
+ final boolean luteAvailable = Markdowns.LUTE_AVAILABLE;
+
+ LOGGER.log(Level.INFO, "Sym is booting [ver=" + VERSION + ", os=" + Latkes.getOperatingSystemName() +
+ ", isDocker=" + Latkes.isDocker() + ", luteAvailable=" + luteAvailable + ", pid=" + Latkes.currentPID() +
+ ", runtimeDatabase=" + runtimeDatabase + ", runtimeMode=" + Latkes.getRuntimeMode() + ", jdbc.username=" +
+ jdbcUsername + ", jdbc.URL=" + jdbcURL + "]");
+
+ final BeanManager beanManager = BeanManager.getInstance();
+
+ final ErrorProcessor errorProcessor = beanManager.getReference(ErrorProcessor.class);
+ Dispatcher.error("/error/{statusCode}", errorProcessor::handleErrorPage);
+
+ final ArticleChannel articleChannel = beanManager.getReference(ArticleChannel.class);
+ Dispatcher.webSocket("/article-channel", articleChannel);
+ final ArticleListChannel articleListChannel = beanManager.getReference(ArticleListChannel.class);
+ Dispatcher.webSocket("/article-list-channel", articleListChannel);
+ final ChatroomChannel chatroomChannel = beanManager.getReference(ChatroomChannel.class);
+ Dispatcher.webSocket("/chat-room-channel", chatroomChannel);
+ final GobangChannel gobangChannel = beanManager.getReference(GobangChannel.class);
+ Dispatcher.webSocket("/gobang-game-channel", gobangChannel);
+ final UserChannel userChannel = beanManager.getReference(UserChannel.class);
+ Dispatcher.webSocket("/user-channel", userChannel);
+
+ final InitMgmtService initMgmtService = beanManager.getReference(InitMgmtService.class);
+ initMgmtService.initSym();
+
+ // Register event listeners
+ final EventManager eventManager = beanManager.getReference(EventManager.class);
+ final ArticleAddNotifier articleAddNotifier = beanManager.getReference(ArticleAddNotifier.class);
+ eventManager.registerListener(articleAddNotifier);
+ final ArticleUpdateNotifier articleUpdateNotifier = beanManager.getReference(ArticleUpdateNotifier.class);
+ eventManager.registerListener(articleUpdateNotifier);
+ final ArticleBaiduSender articleBaiduSender = beanManager.getReference(ArticleBaiduSender.class);
+ eventManager.registerListener(articleBaiduSender);
+ final CommentNotifier commentNotifier = beanManager.getReference(CommentNotifier.class);
+ eventManager.registerListener(commentNotifier);
+ final CommentUpdateNotifier commentUpdateNotifier = beanManager.getReference(CommentUpdateNotifier.class);
+ eventManager.registerListener(commentUpdateNotifier);
+ final ArticleSearchAdder articleSearchAdder = beanManager.getReference(ArticleSearchAdder.class);
+ eventManager.registerListener(articleSearchAdder);
+ final ArticleSearchUpdater articleSearchUpdater = beanManager.getReference(ArticleSearchUpdater.class);
+ eventManager.registerListener(articleSearchUpdater);
+ final ArticleAddAudioHandler articleAddAudioHandler = beanManager.getReference(ArticleAddAudioHandler.class);
+ eventManager.registerListener(articleAddAudioHandler);
+ final ArticleUpdateAudioHandler articleUpdateAudioHandler = beanManager.getReference(ArticleUpdateAudioHandler.class);
+ eventManager.registerListener(articleUpdateAudioHandler);
+
+ final TagCache tagCache = beanManager.getReference(TagCache.class);
+ tagCache.loadTags();
+ final DomainCache domainCache = beanManager.getReference(DomainCache.class);
+ domainCache.loadDomains();
+ final CronMgmtService cronMgmtService = beanManager.getReference(CronMgmtService.class);
+ cronMgmtService.start();
+
+ Stopwatchs.end();
+ LOGGER.log(Level.DEBUG, "Stopwatch: {0}{1}", Strings.LINE_SEPARATOR, Stopwatchs.getTimingStat());
+ Stopwatchs.release();
+
+ final Server server = new Server();
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ cronMgmtService.stop();
+ server.shutdown();
+ Latkes.shutdown();
+
+ Symphonys.SCHEDULED_EXECUTOR_SERVICE.shutdown();
+ Symphonys.EXECUTOR_SERVICE.shutdown();
+ }));
+ server.start(Integer.valueOf(portArg));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/cache/ArticleCache.java b/src/main/java/org/b3log/symphony/cache/ArticleCache.java
new file mode 100644
index 000000000..7b128e87d
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/cache/ArticleCache.java
@@ -0,0 +1,306 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.cache;
+
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.cache.Cache;
+import org.b3log.latke.cache.CacheFactory;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.util.CollectionUtils;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.repository.ArticleRepository;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.util.JSONs;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Article cache.
+ *
+ * @author Liang Ding
+ * @author qiankunpingtai
+ * @version 1.3.1.3, May 20, 2019
+ * @since 1.4.0
+ */
+@Singleton
+public class ArticleCache {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleCache.class);
+
+ /**
+ * Article cache.
+ */
+ private static final Cache ARTICLE_CACHE = CacheFactory.getCache(Article.ARTICLES);
+
+ /**
+ * Article abstract cache.
+ */
+ private static final Cache ARTICLE_ABSTRACT_CACHE = CacheFactory.getCache(Article.ARTICLES + "_"
+ + Article.ARTICLE_T_PREVIEW_CONTENT);
+
+ /**
+ * Side hot articles cache.
+ */
+ private static final List SIDE_HOT_ARTICLES = new ArrayList<>();
+
+ /**
+ * Side random articles cache.
+ */
+ private static final List SIDE_RANDOM_ARTICLES = new ArrayList<>();
+
+ /**
+ * Perfect articles cache.
+ */
+ private static final List PERFECT_ARTICLES = new ArrayList<>();
+
+ /**
+ * Gets side hot articles.
+ *
+ * @return side hot articles
+ */
+ public List getSideHotArticles() {
+ if (SIDE_HOT_ARTICLES.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return JSONs.clone(SIDE_HOT_ARTICLES);
+ }
+
+ /**
+ * Loads side hot articles.
+ */
+ public void loadSideHotArticles() {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class);
+ final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class);
+
+ Stopwatchs.start("Load side hot articles");
+ try {
+ final String id = String.valueOf(DateUtils.addDays(new Date(), -7).getTime());
+ final Query query = new Query().addSort(Article.ARTICLE_COMMENT_CNT, SortDirection.DESCENDING).
+ addSort(Keys.OBJECT_ID, SortDirection.ASCENDING).
+ setPage(1, Symphonys.SIDE_HOT_ARTICLES_CNT);
+ final List filters = new ArrayList<>();
+ filters.add(new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN_OR_EQUAL, id));
+ filters.add(new PropertyFilter(Article.ARTICLE_TYPE, FilterOperator.NOT_EQUAL, Article.ARTICLE_TYPE_C_DISCUSSION));
+ filters.add(new PropertyFilter(Article.ARTICLE_TAGS, FilterOperator.NOT_EQUAL, Tag.TAG_TITLE_C_SANDBOX));
+ filters.add(new PropertyFilter(Article.ARTICLE_SHOW_IN_LIST, FilterOperator.NOT_EQUAL, Article.ARTICLE_SHOW_IN_LIST_C_NOT));
+ query.setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters)).
+ select(Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK, Article.ARTICLE_AUTHOR_ID, Article.ARTICLE_ANONYMOUS);
+ final JSONObject result = articleRepository.get(query);
+ final List articles = CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS));
+ articleQueryService.organizeArticles(articles);
+
+ SIDE_HOT_ARTICLES.clear();
+ SIDE_HOT_ARTICLES.addAll(articles);
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Loads side hot articles failed", e);
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+
+ /**
+ * Gets side random articles.
+ *
+ * @return side random articles
+ */
+ public List getSideRandomArticles() {
+ int size = Symphonys.SIDE_RANDOM_ARTICLES_CNT;
+ if (1 > size) {
+ return Collections.emptyList();
+ }
+
+ if (SIDE_RANDOM_ARTICLES.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ size = size > SIDE_RANDOM_ARTICLES.size() ? SIDE_RANDOM_ARTICLES.size() : size;
+ Collections.shuffle(SIDE_RANDOM_ARTICLES);
+
+ return JSONs.clone(SIDE_RANDOM_ARTICLES.subList(0, size));
+ }
+
+ /**
+ * Loads side random articles.
+ */
+ public void loadSideRandomArticles() {
+ final int size = Symphonys.SIDE_RANDOM_ARTICLES_CNT;
+ if (1 > size) {
+ return;
+ }
+
+ final BeanManager beanManager = BeanManager.getInstance();
+ final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class);
+ final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class);
+
+ Stopwatchs.start("Load side random articles");
+ try {
+ final List articles = articleRepository.getRandomly(size * 5);
+ articleQueryService.organizeArticles(articles);
+
+ SIDE_RANDOM_ARTICLES.clear();
+ SIDE_RANDOM_ARTICLES.addAll(articles);
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Loads side random articles failed", e);
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+
+ /**
+ * Gets an article abstract by the specified article id.
+ *
+ * @param articleId the specified article id
+ * @return article abstract, return {@code null} if not found
+ */
+ public String getArticleAbstract(final String articleId) {
+ final JSONObject value = ARTICLE_ABSTRACT_CACHE.get(articleId);
+ if (null == value) {
+ return null;
+ }
+
+ return value.optString(Common.DATA);
+ }
+
+ /**
+ * Gets perfect articles.
+ *
+ * @return side random articles
+ */
+ public List getPerfectArticles() {
+ if (PERFECT_ARTICLES.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return JSONs.clone(PERFECT_ARTICLES);
+ }
+
+ /**
+ * Loads perfect articles.
+ */
+ public void loadPerfectArticles() {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final ArticleRepository articleRepository = beanManager.getReference(ArticleRepository.class);
+ final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class);
+
+ Stopwatchs.start("Query perfect articles");
+ try {
+ final Query query = new Query().
+ addSort(Keys.OBJECT_ID, SortDirection.DESCENDING).
+ setPageCount(1).setPage(1, 36);
+ query.setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Article.ARTICLE_PERFECT, FilterOperator.EQUAL, Article.ARTICLE_PERFECT_C_PERFECT),
+ new PropertyFilter(Article.ARTICLE_SHOW_IN_LIST, FilterOperator.NOT_EQUAL, Article.ARTICLE_SHOW_IN_LIST_C_NOT)));
+ query.select(Keys.OBJECT_ID,
+ Article.ARTICLE_STICK,
+ Article.ARTICLE_CREATE_TIME,
+ Article.ARTICLE_UPDATE_TIME,
+ Article.ARTICLE_LATEST_CMT_TIME,
+ Article.ARTICLE_AUTHOR_ID,
+ Article.ARTICLE_TITLE,
+ Article.ARTICLE_STATUS,
+ Article.ARTICLE_VIEW_CNT,
+ Article.ARTICLE_TYPE,
+ Article.ARTICLE_PERMALINK,
+ Article.ARTICLE_TAGS,
+ Article.ARTICLE_LATEST_CMTER_NAME,
+ Article.ARTICLE_COMMENT_CNT,
+ Article.ARTICLE_ANONYMOUS,
+ Article.ARTICLE_PERFECT,
+ Article.ARTICLE_QNA_OFFER_POINT,
+ Article.ARTICLE_SHOW_IN_LIST);
+ final JSONObject result = articleRepository.get(query);
+ final List articles = CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS));
+
+ articleQueryService.organizeArticles(articles);
+
+ PERFECT_ARTICLES.clear();
+ PERFECT_ARTICLES.addAll(articles);
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Loads perfect articles failed", e);
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+
+ /**
+ * Puts an article abstract by the specified article id and article abstract.
+ *
+ * @param articleId the specified article id
+ * @param articleAbstract the specified article abstract
+ */
+ public void putArticleAbstract(final String articleId, final String articleAbstract) {
+ final JSONObject value = new JSONObject();
+ value.put(Common.DATA, articleAbstract);
+ ARTICLE_ABSTRACT_CACHE.put(articleId, value);
+ }
+
+ /**
+ * Gets an article by the specified article id.
+ *
+ * @param id the specified article id
+ * @return article, returns {@code null} if not found
+ */
+ public JSONObject getArticle(final String id) {
+ final JSONObject article = ARTICLE_CACHE.get(id);
+ if (null == article) {
+ return null;
+ }
+
+ return JSONs.clone(article);
+ }
+
+ /**
+ * Adds or updates the specified article.
+ *
+ * @param article the specified article
+ */
+ public void putArticle(final JSONObject article) {
+ final String articleId = article.optString(Keys.OBJECT_ID);
+
+ ARTICLE_CACHE.put(articleId, JSONs.clone(article));
+ ARTICLE_ABSTRACT_CACHE.remove(articleId);
+ }
+
+ /**
+ * Removes an article by the specified article id.
+ *
+ * @param id the specified article id
+ */
+ public void removeArticle(final String id) {
+ ARTICLE_CACHE.remove(id);
+ ARTICLE_ABSTRACT_CACHE.remove(id);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/cache/CommentCache.java b/src/main/java/org/b3log/symphony/cache/CommentCache.java
new file mode 100644
index 000000000..019d6421b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/cache/CommentCache.java
@@ -0,0 +1,75 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.cache;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.cache.Cache;
+import org.b3log.latke.cache.CacheFactory;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.symphony.model.Comment;
+import org.b3log.symphony.util.JSONs;
+import org.json.JSONObject;
+
+/**
+ * Comment cache.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Sep 1, 2016
+ * @since 1.6.0
+ */
+@Singleton
+public class CommentCache {
+
+ /**
+ * Comment cache.
+ */
+ private static final Cache cache = CacheFactory.getCache(Comment.COMMENTS);
+
+ /**
+ * Gets a comment by the specified comment id.
+ *
+ * @param id the specified comment id
+ * @return comment, returns {@code null} if not found
+ */
+ public JSONObject getComment(final String id) {
+ final JSONObject comment = cache.get(id);
+ if (null == comment) {
+ return null;
+ }
+
+ return JSONs.clone(comment);
+ }
+
+ /**
+ * Adds or updates the specified comment.
+ *
+ * @param comment the specified comment
+ */
+ public void putComment(final JSONObject comment) {
+ cache.put(comment.optString(Keys.OBJECT_ID), JSONs.clone(comment));
+ }
+
+ /**
+ * Removes a comment by the specified comment id.
+ *
+ * @param id the specified comment id
+ */
+ public void removeComment(final String id) {
+ cache.remove(id);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/cache/DomainCache.java b/src/main/java/org/b3log/symphony/cache/DomainCache.java
new file mode 100644
index 000000000..b115d25ab
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/cache/DomainCache.java
@@ -0,0 +1,97 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.cache;
+
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.service.DomainQueryService;
+import org.b3log.symphony.util.JSONs;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Domain cache.
+ *
+ * @author Liang Ding
+ * @version 1.0.2.3, Aug 31, 2018
+ * @since 1.4.0
+ */
+@Singleton
+public class DomainCache {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(DomainCache.class);
+
+ /**
+ * Domains.
+ */
+ private static final List DOMAINS = new ArrayList<>();
+
+ /**
+ * Lock.
+ */
+ private static final ReadWriteLock LOCK = new ReentrantReadWriteLock();
+
+ /**
+ * Domain query service.
+ */
+ @Inject
+ private DomainQueryService domainQueryService;
+
+ /**
+ * Gets domains with the specified fetch size.
+ *
+ * @param fetchSize the specified fetch size
+ * @return domains
+ */
+ public List getDomains(final int fetchSize) {
+ LOCK.readLock().lock();
+ try {
+ if (DOMAINS.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final int end = fetchSize >= DOMAINS.size() ? DOMAINS.size() : fetchSize;
+
+ return JSONs.clone(DOMAINS.subList(0, end));
+ } finally {
+ LOCK.readLock().unlock();
+ }
+ }
+
+ /**
+ * Loads domains.
+ */
+ public void loadDomains() {
+ LOCK.writeLock().lock();
+ try {
+ DOMAINS.clear();
+ DOMAINS.addAll(domainQueryService.getMostTagNaviDomains(Integer.MAX_VALUE));
+ } finally {
+ LOCK.writeLock().unlock();
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/cache/OptionCache.java b/src/main/java/org/b3log/symphony/cache/OptionCache.java
new file mode 100644
index 000000000..203d08cb2
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/cache/OptionCache.java
@@ -0,0 +1,75 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.cache;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.cache.Cache;
+import org.b3log.latke.cache.CacheFactory;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.util.JSONs;
+import org.json.JSONObject;
+
+/**
+ * Option cache.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Oct 25, 2016
+ * @since 1.5.0
+ */
+@Singleton
+public class OptionCache {
+
+ /**
+ * Option cache.
+ */
+ private static final Cache CACHE = CacheFactory.getCache(Option.OPTIONS);
+
+ /**
+ * Gets an option by the specified option id.
+ *
+ * @param id the specified option id
+ * @return option, returns {@code null} if not found
+ */
+ public JSONObject getOption(final String id) {
+ final JSONObject option = CACHE.get(id);
+ if (null == option) {
+ return null;
+ }
+
+ return JSONs.clone(option);
+ }
+
+ /**
+ * Adds or updates the specified option.
+ *
+ * @param option the specified option
+ */
+ public void putOption(final JSONObject option) {
+ CACHE.put(option.optString(Keys.OBJECT_ID), JSONs.clone(option));
+ }
+
+ /**
+ * Removes an option by the specified option id.
+ *
+ * @param id the specified option id
+ */
+ public void removeOption(final String id) {
+ CACHE.remove(id);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/cache/TagCache.java b/src/main/java/org/b3log/symphony/cache/TagCache.java
new file mode 100644
index 000000000..5eec5b990
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/cache/TagCache.java
@@ -0,0 +1,304 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.cache;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.util.CollectionUtils;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.repository.TagRepository;
+import org.b3log.symphony.util.JSONs;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tag cache.
+ *
+ * @author Liang Ding
+ * @version 1.5.6.5, Aug 31, 2018
+ * @since 1.4.0
+ */
+@Singleton
+public class TagCache {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(TagCache.class);
+
+ /**
+ * Icon tags.
+ */
+ private static final List ICON_TAGS = new ArrayList<>();
+
+ /**
+ * New tags.
+ */
+ private static final List NEW_TAGS = new ArrayList<>();
+
+ /**
+ * All tags.
+ */
+ private static final List TAGS = new ArrayList<>();
+
+ /**
+ * <title, URI>
+ */
+ private static final Map TITLE_URIS = new ConcurrentHashMap<>();
+
+ /**
+ * <id, tag>
+ */
+ private static final Map CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * Gets a tag by the specified tag id.
+ *
+ * @param id the specified tag id
+ * @return tag, returns {@code null} if not found
+ */
+ public JSONObject getTag(final String id) {
+ final JSONObject tag = CACHE.get(id);
+ if (null == tag) {
+ return null;
+ }
+
+ final JSONObject ret = JSONs.clone(tag);
+
+ TITLE_URIS.put(ret.optString(Tag.TAG_TITLE), ret.optString(Tag.TAG_URI));
+
+ return ret;
+ }
+
+ /**
+ * Adds or updates the specified tag.
+ *
+ * @param tag the specified tag
+ */
+ public void putTag(final JSONObject tag) {
+ CACHE.put(tag.optString(Keys.OBJECT_ID), JSONs.clone(tag));
+
+ TITLE_URIS.put(tag.optString(Tag.TAG_TITLE), tag.optString(Tag.TAG_URI));
+ }
+
+ /**
+ * Removes a tag by the specified tag id.
+ *
+ * @param id the specified tag id
+ */
+ public void removeTag(final String id) {
+ final JSONObject tag = CACHE.get(id);
+ if (null == tag) {
+ return;
+ }
+
+ CACHE.remove(id);
+
+ TITLE_URIS.remove(tag.optString(Tag.TAG_TITLE));
+ }
+
+ /**
+ * Gets a tag URI with the specified tag title.
+ *
+ * @param title the specified tag title
+ * @return tag URI, returns {@code null} if not found
+ */
+ public String getURIByTitle(final String title) {
+ return TITLE_URIS.get(title);
+ }
+
+ /**
+ * Gets new tags with the specified fetch size.
+ *
+ * @return new tags
+ */
+ public List getNewTags() {
+ if (NEW_TAGS.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return JSONs.clone(NEW_TAGS);
+ }
+
+ /**
+ * Gets icon tags with the specified fetch size.
+ *
+ * @param fetchSize the specified fetch size
+ * @return icon tags
+ */
+ public List getIconTags(final int fetchSize) {
+ if (ICON_TAGS.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final int end = fetchSize >= ICON_TAGS.size() ? ICON_TAGS.size() : fetchSize;
+
+ return JSONs.clone(ICON_TAGS.subList(0, end));
+ }
+
+ /**
+ * Gets all tags.
+ *
+ * @return all tags
+ */
+ public List getTags() {
+ if (TAGS.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ return JSONs.clone(TAGS);
+ }
+
+ /**
+ * Loads all tags.
+ */
+ public void loadTags() {
+ loadAllTags();
+ loadIconTags();
+ loadNewTags();
+ }
+
+ /**
+ * Loads new tags.
+ */
+ private void loadNewTags() {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final TagRepository tagRepository = beanManager.getReference(TagRepository.class);
+
+ final Query query = new Query().addSort(Keys.OBJECT_ID, SortDirection.DESCENDING).
+ setPage(1, Symphonys.SIDE_TAGS_CNT).setPageCount(1);
+
+ query.setFilter(new PropertyFilter(Tag.TAG_REFERENCE_CNT, FilterOperator.GREATER_THAN, 0));
+
+ try {
+ final JSONObject result = tagRepository.get(query);
+ NEW_TAGS.clear();
+ NEW_TAGS.addAll(CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS)));
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Gets new tags failed", e);
+ }
+ }
+
+ /**
+ * Loads icon tags.
+ */
+ private void loadIconTags() {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final TagRepository tagRepository = beanManager.getReference(TagRepository.class);
+
+ final Query query = new Query().
+ setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Tag.TAG_ICON_PATH, FilterOperator.NOT_EQUAL, ""),
+ new PropertyFilter(Tag.TAG_STATUS, FilterOperator.EQUAL, Tag.TAG_STATUS_C_VALID))).
+ setPage(1, Integer.MAX_VALUE).setPageCount(1).
+ addSort(Tag.TAG_RANDOM_DOUBLE, SortDirection.ASCENDING);
+ try {
+ final JSONObject result = tagRepository.get(query);
+ final List tags = CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS));
+ final List toUpdateTags = new ArrayList<>();
+ for (final JSONObject tag : tags) {
+ toUpdateTags.add(JSONs.clone(tag));
+ }
+
+ for (final JSONObject tag : tags) {
+ Tag.fillDescription(tag);
+ tag.put(Tag.TAG_T_TITLE_LOWER_CASE, tag.optString(Tag.TAG_TITLE).toLowerCase());
+ }
+
+ ICON_TAGS.clear();
+ ICON_TAGS.addAll(tags);
+
+ // Updates random double
+ final Transaction transaction = tagRepository.beginTransaction();
+ for (final JSONObject tag : toUpdateTags) {
+ tag.put(Tag.TAG_RANDOM_DOUBLE, Math.random());
+
+ tagRepository.update(tag.optString(Keys.OBJECT_ID), tag);
+ }
+ transaction.commit();
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Load icon tags failed", e);
+ }
+ }
+
+ /**
+ * Loads all tags.
+ */
+ public void loadAllTags() {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final TagRepository tagRepository = beanManager.getReference(TagRepository.class);
+
+ final Query query = new Query().
+ setFilter(new PropertyFilter(Tag.TAG_STATUS, FilterOperator.EQUAL, Tag.TAG_STATUS_C_VALID)).
+ setPage(1, Integer.MAX_VALUE).setPageCount(1);
+ try {
+ final JSONObject result = tagRepository.get(query);
+ final List tags = CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS));
+
+ final Iterator iterator = tags.iterator();
+ while (iterator.hasNext()) {
+ final JSONObject tag = iterator.next();
+
+ String title = tag.optString(Tag.TAG_TITLE);
+ if ("".equals(title)
+ || StringUtils.contains(title, " ")
+ || StringUtils.contains(title, " ")) { // filter legacy data
+ iterator.remove();
+
+ continue;
+ }
+
+ if (!Tag.containsWhiteListTags(title)) {
+ if (!Tag.TAG_TITLE_PATTERN.matcher(title).matches() || title.length() > Tag.MAX_TAG_TITLE_LENGTH) {
+ iterator.remove();
+
+ continue;
+ }
+ }
+
+ Tag.fillDescription(tag);
+ tag.put(Tag.TAG_T_TITLE_LOWER_CASE, tag.optString(Tag.TAG_TITLE).toLowerCase());
+ }
+
+ tags.sort((t1, t2) -> {
+ final String u1Title = t1.optString(Tag.TAG_T_TITLE_LOWER_CASE);
+ final String u2Title = t2.optString(Tag.TAG_T_TITLE_LOWER_CASE);
+
+ return u1Title.compareTo(u2Title);
+ });
+
+ TAGS.clear();
+ TAGS.addAll(tags);
+
+ TITLE_URIS.clear();
+ for (final JSONObject tag : tags) {
+ TITLE_URIS.put(tag.optString(Tag.TAG_TITLE), tag.optString(Tag.TAG_URI));
+ }
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Load all tags failed", e);
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/cache/UserCache.java b/src/main/java/org/b3log/symphony/cache/UserCache.java
new file mode 100644
index 000000000..fb2f1bec3
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/cache/UserCache.java
@@ -0,0 +1,127 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.cache;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.util.JSONs;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * User cache.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.1, Jul 30, 2018
+ * @since 1.4.0
+ */
+@Singleton
+public class UserCache {
+
+ /**
+ * Id, User.
+ */
+ private static final Map ID_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * Name, User.
+ */
+ private static final Map NAME_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * Administrators cache.
+ */
+ private static final List ADMINS_CACHE = new CopyOnWriteArrayList<>();
+
+ /**
+ * Gets admins.
+ *
+ * @return admins
+ */
+ public List getAdmins() {
+ return ADMINS_CACHE;
+ }
+
+ /**
+ * Puts the specified admins.
+ *
+ * @param admins the specified admins
+ */
+ public void putAdmins(final List admins) {
+ ADMINS_CACHE.clear();
+ ADMINS_CACHE.addAll(admins);
+ }
+
+ /**
+ * Gets a user by the specified user id.
+ *
+ * @param userId the specified user id
+ * @return user, returns {@code null} if not found
+ */
+ public JSONObject getUser(final String userId) {
+ final JSONObject user = ID_CACHE.get(userId);
+ if (null == user) {
+ return null;
+ }
+
+ return JSONs.clone(user);
+ }
+
+ /**
+ * Gets a user by the specified user name.
+ *
+ * @param userName the specified user name
+ * @return user, returns {@code null} if not found
+ */
+ public JSONObject getUserByName(final String userName) {
+ final JSONObject user = NAME_CACHE.get(userName);
+ if (null == user) {
+ return null;
+ }
+
+ return JSONs.clone(user);
+ }
+
+ /**
+ * Adds or updates the specified user.
+ *
+ * @param user the specified user
+ */
+ public void putUser(final JSONObject user) {
+ ID_CACHE.put(user.optString(Keys.OBJECT_ID), JSONs.clone(user));
+ NAME_CACHE.put(user.optString(User.USER_NAME), JSONs.clone(user));
+
+ Sessions.put(user.optString(Keys.OBJECT_ID), user);
+ }
+
+ /**
+ * Removes the the specified user.
+ *
+ * @param user the specified user
+ */
+ public void removeUser(final JSONObject user) {
+ ID_CACHE.remove(user.optString(Keys.OBJECT_ID));
+ NAME_CACHE.remove(user.optString(User.USER_NAME));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleAddAudioHandler.java b/src/main/java/org/b3log/symphony/event/ArticleAddAudioHandler.java
new file mode 100644
index 000000000..c42fc8fd1
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleAddAudioHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.service.ArticleMgmtService;
+import org.json.JSONObject;
+
+/**
+ * Article add audio handler.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Nov 3, 2018
+ * @since 2.1.0
+ */
+@Singleton
+public class ArticleAddAudioHandler extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleAddAudioHandler.class);
+
+ /**
+ * Article management service.
+ */
+ @Inject
+ private ArticleMgmtService articleMgmtService;
+
+ /**
+ * Gets the event type {@linkplain EventTypes#ADD_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.ADD_ARTICLE;
+ }
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ final JSONObject originalArticle = data.optJSONObject(Article.ARTICLE);
+ articleMgmtService.genArticleAudio(originalArticle);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleAddNotifier.java b/src/main/java/org/b3log/symphony/event/ArticleAddNotifier.java
new file mode 100644
index 000000000..0cce7e4d3
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleAddNotifier.java
@@ -0,0 +1,218 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.service.FollowQueryService;
+import org.b3log.symphony.service.NotificationMgmtService;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Escapes;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Sends article add related notifications.
+ *
+ * @author Liang Ding
+ * @version 1.3.4.19, Apr 9, 2019
+ * @since 0.2.0
+ */
+@Singleton
+public class ArticleAddNotifier extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleAddNotifier.class);
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Follow query service.
+ */
+ @Inject
+ private FollowQueryService followQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ try {
+ final JSONObject originalArticle = data.getJSONObject(Article.ARTICLE);
+ final String articleId = originalArticle.optString(Keys.OBJECT_ID);
+ final String articleAuthorId = originalArticle.optString(Article.ARTICLE_AUTHOR_ID);
+ final JSONObject articleAuthor = userQueryService.getUser(articleAuthorId);
+ final String articleAuthorName = articleAuthor.optString(User.USER_NAME);
+ final Set requisiteAtUserPermissions = new HashSet<>();
+ requisiteAtUserPermissions.add(Permission.PERMISSION_ID_C_COMMON_AT_USER);
+ final boolean hasAtUserPerm = roleQueryService.userHasPermissions(articleAuthorId, requisiteAtUserPermissions);
+ final Set atedUserIds = new HashSet<>();
+ if (hasAtUserPerm) {
+ // 'At' Notification
+ final String articleContent = originalArticle.optString(Article.ARTICLE_CONTENT);
+ final Set atUserNames = userQueryService.getUserNames(articleContent);
+ atUserNames.remove(articleAuthorName); // Do not notify the author itself
+ for (final String userName : atUserNames) {
+ final JSONObject user = userQueryService.getUserByName(userName);
+ final JSONObject requestJSONObject = new JSONObject();
+ final String atedUserId = user.optString(Keys.OBJECT_ID);
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, atedUserId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, articleId);
+ notificationMgmtService.addAtNotification(requestJSONObject);
+
+ atedUserIds.add(atedUserId);
+ }
+ }
+
+ final String tags = originalArticle.optString(Article.ARTICLE_TAGS);
+
+ // 'following - user' Notification
+ final boolean articleNotifyFollowers = data.optBoolean(Article.ARTICLE_T_NOTIFY_FOLLOWERS);
+ if (articleNotifyFollowers
+ && Article.ARTICLE_TYPE_C_DISCUSSION != originalArticle.optInt(Article.ARTICLE_TYPE)
+ && Article.ARTICLE_ANONYMOUS_C_PUBLIC == originalArticle.optInt(Article.ARTICLE_ANONYMOUS)
+ && !Tag.TAG_TITLE_C_SANDBOX.equals(tags)
+ && !StringUtils.containsIgnoreCase(tags, Symphonys.SYS_ANNOUNCE_TAG)) {
+ final JSONObject followerUsersResult = followQueryService.getFollowerUsers(articleAuthorId, 1, Integer.MAX_VALUE);
+ final List followerUsers = (List) followerUsersResult.opt(Keys.RESULTS);
+ final long thirtyDaysAgo = DateUtils.addDays(new Date(), -30).getTime();
+ for (final JSONObject followerUser : followerUsers) {
+ // 30 天未登录的用户不发关注发帖通知 https://github.com/b3log/symphony/issues/820
+ final long latestLoginTime = followerUser.optLong(UserExt.USER_LATEST_LOGIN_TIME);
+ if (latestLoginTime < thirtyDaysAgo) {
+ continue;
+ }
+
+ final JSONObject requestJSONObject = new JSONObject();
+ final String followerUserId = followerUser.optString(Keys.OBJECT_ID);
+ if (atedUserIds.contains(followerUserId)) {
+ continue;
+ }
+
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, followerUserId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, articleId);
+ notificationMgmtService.addFollowingUserNotification(requestJSONObject);
+ }
+ }
+
+ final String articleTitle = Escapes.escapeHTML(originalArticle.optString(Article.ARTICLE_TITLE));
+
+ // 'Broadcast' Notification
+ if (Article.ARTICLE_TYPE_C_CITY_BROADCAST == originalArticle.optInt(Article.ARTICLE_TYPE)
+ && Article.ARTICLE_ANONYMOUS_C_PUBLIC == originalArticle.optInt(Article.ARTICLE_ANONYMOUS)) {
+ final String city = originalArticle.optString(Article.ARTICLE_CITY);
+ if (StringUtils.isNotBlank(city)) {
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, 1);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, Integer.MAX_VALUE);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, Integer.MAX_VALUE);
+ final long latestLoginTime = DateUtils.addDays(new Date(), -15).getTime();
+ requestJSONObject.put(UserExt.USER_LATEST_LOGIN_TIME, latestLoginTime);
+ requestJSONObject.put(UserExt.USER_CITY, city);
+ final JSONObject result = userQueryService.getUsersByCity(requestJSONObject);
+ final JSONArray users = result.optJSONArray(User.USERS);
+ for (int i = 0; i < users.length(); i++) {
+ final String userId = users.optJSONObject(i).optString(Keys.OBJECT_ID);
+ if (userId.equals(articleAuthorId)) {
+ continue;
+ }
+
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, userId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, articleId);
+ notificationMgmtService.addBroadcastNotification(notification);
+ }
+
+ LOGGER.info("City [" + city + "] broadcast [users=" + users.length() + "]");
+ }
+ }
+
+ // 'Sys Announce' Notification
+ if (StringUtils.containsIgnoreCase(tags, Symphonys.SYS_ANNOUNCE_TAG)) {
+ final long latestLoginTime = DateUtils.addDays(new Date(), -15).getTime();
+
+ final JSONObject result = userQueryService.getLatestLoggedInUsers(
+ latestLoginTime, 1, Integer.MAX_VALUE, Integer.MAX_VALUE);
+ final JSONArray users = result.optJSONArray(User.USERS);
+ for (int i = 0; i < users.length(); i++) {
+ final String userId = users.optJSONObject(i).optString(Keys.OBJECT_ID);
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, userId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, articleId);
+ notificationMgmtService.addSysAnnounceArticleNotification(notification);
+ }
+
+ LOGGER.info("System announcement [" + articleTitle + "] broadcast [users=" + users.length() + "]");
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Sends the article add notification failed", e);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#ADD_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.ADD_ARTICLE;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleBaiduSender.java b/src/main/java/org/b3log/symphony/event/ArticleBaiduSender.java
new file mode 100644
index 000000000..2a271ee50
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleBaiduSender.java
@@ -0,0 +1,120 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import jodd.net.MimeTypes;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+/**
+ * Sends an article URL to Baidu.
+ *
+ * @author Liang Ding
+ * @version 1.1.3.4, Jan 12, 2019
+ * @since 1.3.0
+ */
+@Singleton
+public class ArticleBaiduSender extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleBaiduSender.class);
+
+ /**
+ * Sends the specified URLs to Baidu.
+ *
+ * @param urls the specified URLs
+ */
+ public static void sendToBaidu(final String... urls) {
+ if (Latkes.RuntimeMode.PRODUCTION != Latkes.getRuntimeMode() || StringUtils.isBlank(Symphonys.BAIDU_DATA_TOKEN)) {
+ return;
+ }
+
+ if (ArrayUtils.isEmpty(urls)) {
+ return;
+ }
+
+ Symphonys.EXECUTOR_SERVICE.submit(() -> {
+ try {
+ final String urlsStr = StringUtils.join(urls, "\n");
+ final HttpResponse response = HttpRequest.post("http://data.zz.baidu.com/urls?site=" + Latkes.getServerHost() + "&token=" + Symphonys.BAIDU_DATA_TOKEN).
+ header(Common.USER_AGENT, "curl/7.12.1").
+ header("Host", "data.zz.baidu.com").
+ header("Content-Type", "text/plain").
+ header("Connection", "close").body(urlsStr.getBytes(), MimeTypes.MIME_TEXT_PLAIN).timeout(30000).send();
+ response.charset("UTF-8");
+ LOGGER.info("Sent [" + urlsStr + "] to Baidu [response=" + response.bodyText() + "]");
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Ping Baidu spider failed", e);
+ }
+ });
+ }
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ if (Latkes.RuntimeMode.PRODUCTION != Latkes.getRuntimeMode() || StringUtils.isBlank(Symphonys.BAIDU_DATA_TOKEN)) {
+ return;
+ }
+
+ try {
+ final JSONObject article = data.getJSONObject(Article.ARTICLE);
+ final int articleType = article.optInt(Article.ARTICLE_TYPE);
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == articleType || Article.ARTICLE_TYPE_C_THOUGHT == articleType) {
+ return;
+ }
+
+ final String tags = article.optString(Article.ARTICLE_TAGS);
+ if (StringUtils.containsIgnoreCase(tags, Tag.TAG_TITLE_C_SANDBOX)) {
+ return;
+ }
+
+ final String articlePermalink = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK);
+
+ sendToBaidu(articlePermalink);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Sends the article to Baidu error", e);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#ADD_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.ADD_ARTICLE;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleSearchAdder.java b/src/main/java/org/b3log/symphony/event/ArticleSearchAdder.java
new file mode 100644
index 000000000..498bd66fa
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleSearchAdder.java
@@ -0,0 +1,89 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.service.SearchMgmtService;
+import org.b3log.symphony.util.JSONs;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+/**
+ * Sends an article to search engine.
+ *
+ * @author Liang Ding
+ * @version 1.1.3.3, Aug 31, 2018
+ * @since 1.4.0
+ */
+@Singleton
+public class ArticleSearchAdder extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleSearchAdder.class);
+
+ /**
+ * Search management service.
+ */
+ @Inject
+ private SearchMgmtService searchMgmtService;
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ final JSONObject article = data.optJSONObject(Article.ARTICLE);
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)
+ || Article.ARTICLE_TYPE_C_THOUGHT == article.optInt(Article.ARTICLE_TYPE)) {
+ return;
+ }
+
+ final String tags = article.optString(Article.ARTICLE_TAGS);
+ if (StringUtils.containsIgnoreCase(tags, Tag.TAG_TITLE_C_SANDBOX)) {
+ return;
+ }
+
+ if (Symphonys.ALGOLIA_ENABLED) {
+ searchMgmtService.updateAlgoliaDocument(JSONs.clone(article));
+ }
+
+ if (Symphonys.ES_ENABLED) {
+ searchMgmtService.updateESDocument(JSONs.clone(article), Article.ARTICLE);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#ADD_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.ADD_ARTICLE;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleSearchUpdater.java b/src/main/java/org/b3log/symphony/event/ArticleSearchUpdater.java
new file mode 100644
index 000000000..04fa9c9c2
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleSearchUpdater.java
@@ -0,0 +1,89 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.service.SearchMgmtService;
+import org.b3log.symphony.util.JSONs;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+/**
+ * Sends an article to search engine.
+ *
+ * @author Liang Ding
+ * @version 1.1.3.3, Aug 31, 2018
+ * @since 1.4.0
+ */
+@Singleton
+public class ArticleSearchUpdater extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleSearchUpdater.class);
+
+ /**
+ * Search management service.
+ */
+ @Inject
+ private SearchMgmtService searchMgmtService;
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ final JSONObject article = data.optJSONObject(Article.ARTICLE);
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)
+ || Article.ARTICLE_TYPE_C_THOUGHT == article.optInt(Article.ARTICLE_TYPE)) {
+ return;
+ }
+
+ final String tags = article.optString(Article.ARTICLE_TAGS);
+ if (StringUtils.containsIgnoreCase(tags, Tag.TAG_TITLE_C_SANDBOX)) {
+ return;
+ }
+
+ if (Symphonys.ALGOLIA_ENABLED) {
+ searchMgmtService.updateAlgoliaDocument(JSONs.clone(article));
+ }
+
+ if (Symphonys.ES_ENABLED) {
+ searchMgmtService.updateESDocument(JSONs.clone(article), Article.ARTICLE);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#UPDATE_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.UPDATE_ARTICLE;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleUpdateAudioHandler.java b/src/main/java/org/b3log/symphony/event/ArticleUpdateAudioHandler.java
new file mode 100644
index 000000000..1ac6ea052
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleUpdateAudioHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.service.ArticleMgmtService;
+import org.json.JSONObject;
+
+/**
+ * Article update audio handler.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Nov 3, 2018
+ * @since 2.1.0
+ */
+@Singleton
+public class ArticleUpdateAudioHandler extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleUpdateAudioHandler.class);
+
+ /**
+ * Article management service.
+ */
+ @Inject
+ private ArticleMgmtService articleMgmtService;
+
+ /**
+ * Gets the event type {@linkplain EventTypes#UPDATE_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.UPDATE_ARTICLE;
+ }
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ final JSONObject originalArticle = data.optJSONObject(Article.ARTICLE);
+ articleMgmtService.genArticleAudio(originalArticle);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/ArticleUpdateNotifier.java b/src/main/java/org/b3log/symphony/event/ArticleUpdateNotifier.java
new file mode 100644
index 000000000..a636b354d
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/ArticleUpdateNotifier.java
@@ -0,0 +1,170 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.repository.NotificationRepository;
+import org.b3log.symphony.service.FollowQueryService;
+import org.b3log.symphony.service.NotificationMgmtService;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Sends article update related notifications.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.6, Apr 9, 2019
+ * @since 2.0.0
+ */
+@Singleton
+public class ArticleUpdateNotifier extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleUpdateNotifier.class);
+
+ /**
+ * Notification repository.
+ */
+ @Inject
+ private NotificationRepository notificationRepository;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Follow query service.
+ */
+ @Inject
+ private FollowQueryService followQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ try {
+ final JSONObject articleUpdated = data.getJSONObject(Article.ARTICLE);
+ final String articleId = articleUpdated.optString(Keys.OBJECT_ID);
+
+ final String articleAuthorId = articleUpdated.optString(Article.ARTICLE_AUTHOR_ID);
+ final JSONObject articleAuthor = userQueryService.getUser(articleAuthorId);
+ final String articleAuthorName = articleAuthor.optString(User.USER_NAME);
+ final boolean isDiscussion = articleUpdated.optInt(Article.ARTICLE_TYPE) == Article.ARTICLE_TYPE_C_DISCUSSION;
+
+ final String articleContent = articleUpdated.optString(Article.ARTICLE_CONTENT);
+ final Set atUserNames = userQueryService.getUserNames(articleContent);
+ atUserNames.remove(articleAuthorName); // Do not notify the author itself
+
+ final Set requisiteAtUserPermissions = new HashSet<>();
+ requisiteAtUserPermissions.add(Permission.PERMISSION_ID_C_COMMON_AT_USER);
+ final boolean hasAtUserPerm = roleQueryService.userHasPermissions(articleAuthorId, requisiteAtUserPermissions);
+ final Set atedUserIds = new HashSet<>();
+ if (hasAtUserPerm) {
+ // 'At' Notification
+ for (final String userName : atUserNames) {
+ final JSONObject user = userQueryService.getUserByName(userName);
+ final JSONObject requestJSONObject = new JSONObject();
+ final String atedUserId = user.optString(Keys.OBJECT_ID);
+ if (!notificationRepository.hasSentByDataIdAndType(atedUserId, articleId, Notification.DATA_TYPE_C_AT)) {
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, atedUserId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, articleId);
+ notificationMgmtService.addAtNotification(requestJSONObject);
+ }
+
+ atedUserIds.add(atedUserId);
+ }
+ }
+
+ final JSONObject oldArticle = data.optJSONObject(Common.OLD_ARTICLE);
+ if (!Article.isDifferent(oldArticle, articleUpdated)) {
+ // 更新帖子通知改进 https://github.com/b3log/symphony/issues/872
+ LOGGER.log(Level.DEBUG, "The article [title=" + oldArticle.optString(Article.ARTICLE_TITLE) + "] has not changed, do not notify it's watchers");
+
+ return;
+ }
+
+ // 'following - article update' Notification
+ final boolean articleNotifyFollowers = data.optBoolean(Article.ARTICLE_T_NOTIFY_FOLLOWERS);
+ if (articleNotifyFollowers) {
+ final JSONObject followerUsersResult = followQueryService.getArticleWatchers(UserExt.USER_AVATAR_VIEW_MODE_C_ORIGINAL, articleId, 1, Integer.MAX_VALUE);
+ final List watcherUsers = (List) followerUsersResult.opt(Keys.RESULTS);
+ for (final JSONObject watcherUser : watcherUsers) {
+ final String watcherName = watcherUser.optString(User.USER_NAME);
+ if ((isDiscussion && !atUserNames.contains(watcherName)) || articleAuthorName.equals(watcherName)) {
+ continue;
+ }
+
+ final JSONObject requestJSONObject = new JSONObject();
+ final String watcherUserId = watcherUser.optString(Keys.OBJECT_ID);
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, watcherUserId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, articleId);
+ notificationMgmtService.addFollowingArticleUpdateNotification(requestJSONObject);
+ }
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Sends the article update notification failed", e);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#UPDATE_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.UPDATE_ARTICLE;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/CommentNotifier.java b/src/main/java/org/b3log/symphony/event/CommentNotifier.java
new file mode 100644
index 000000000..d1776a29e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/CommentNotifier.java
@@ -0,0 +1,384 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.model.User;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.channel.ArticleChannel;
+import org.b3log.symphony.processor.channel.ArticleListChannel;
+import org.b3log.symphony.repository.CommentRepository;
+import org.b3log.symphony.repository.UserRepository;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Sends a comment notification.
+ *
+ * @author Liang Ding
+ * @version 1.7.13.0, Apr 11, 2019
+ * @since 0.2.0
+ */
+@Singleton
+public class CommentNotifier extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(CommentNotifier.class);
+
+ /**
+ * Comment repository.
+ */
+ @Inject
+ private CommentRepository commentRepository;
+
+ /**
+ * User repository.
+ */
+ @Inject
+ private UserRepository userRepository;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Avatar query service.
+ */
+ @Inject
+ private AvatarQueryService avatarQueryService;
+
+ /**
+ * Short link query service.
+ */
+ @Inject
+ private ShortLinkQueryService shortLinkQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Pointtransfer management service.
+ */
+ @Inject
+ private PointtransferMgmtService pointtransferMgmtService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Follow query service.
+ */
+ @Inject
+ private FollowQueryService followQueryService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ try {
+ final JSONObject originalArticle = data.getJSONObject(Article.ARTICLE);
+ final JSONObject originalComment = data.getJSONObject(Comment.COMMENT);
+ final int commentViewMode = data.optInt(UserExt.USER_COMMENT_VIEW_MODE);
+
+ final String articleId = originalArticle.optString(Keys.OBJECT_ID);
+ final String commentId = originalComment.optString(Keys.OBJECT_ID);
+ final String originalCmtId = originalComment.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID);
+ final String commenterId = originalComment.optString(Comment.COMMENT_AUTHOR_ID);
+
+ final String commentContent = originalComment.optString(Comment.COMMENT_CONTENT);
+ final JSONObject commenter = userQueryService.getUser(commenterId);
+ final String commenterName = commenter.optString(User.USER_NAME);
+
+ // 0. Data channel (WebSocket)
+ final JSONObject chData = JSONs.clone(originalComment);
+ chData.put(Comment.COMMENT_T_COMMENTER, commenter);
+ chData.put(Keys.OBJECT_ID, commentId);
+ chData.put(Article.ARTICLE_T_ID, articleId);
+ chData.put(Article.ARTICLE, originalArticle);
+ chData.put(Comment.COMMENT_T_ID, commentId);
+ chData.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, originalCmtId);
+
+ String originalCmtAuthorId = null;
+ if (StringUtils.isNotBlank(originalCmtId)) {
+ final Query numQuery = new Query().setPage(1, Integer.MAX_VALUE).setPageCount(1);
+
+ switch (commentViewMode) {
+ case UserExt.USER_COMMENT_VIEW_MODE_C_TRADITIONAL:
+ numQuery.setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId),
+ new PropertyFilter(Keys.OBJECT_ID, FilterOperator.LESS_THAN_OR_EQUAL, originalCmtId)
+ )).addSort(Keys.OBJECT_ID, SortDirection.ASCENDING);
+
+ break;
+ case UserExt.USER_COMMENT_VIEW_MODE_C_REALTIME:
+ numQuery.setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId),
+ new PropertyFilter(Keys.OBJECT_ID, FilterOperator.GREATER_THAN_OR_EQUAL, originalCmtId)
+ )).addSort(Keys.OBJECT_ID, SortDirection.DESCENDING);
+
+ break;
+ }
+
+ final long num = commentRepository.count(numQuery);
+ final int page = (int) ((num / Symphonys.ARTICLE_COMMENTS_CNT) + 1);
+ chData.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, page);
+
+ final JSONObject originalCmt = commentRepository.get(originalCmtId);
+ originalCmtAuthorId = originalCmt.optString(Comment.COMMENT_AUTHOR_ID);
+ final JSONObject originalCmtAuthor = userRepository.get(originalCmtAuthorId);
+
+ if (Comment.COMMENT_ANONYMOUS_C_PUBLIC == originalCmt.optInt(Comment.COMMENT_ANONYMOUS)) {
+ chData.put(Comment.COMMENT_T_ORIGINAL_AUTHOR_THUMBNAIL_URL,
+ avatarQueryService.getAvatarURLByUser(originalCmtAuthor, "20"));
+ } else {
+ chData.put(Comment.COMMENT_T_ORIGINAL_AUTHOR_THUMBNAIL_URL,
+ avatarQueryService.getDefaultAvatarURL("20"));
+ }
+ }
+
+ if (Comment.COMMENT_ANONYMOUS_C_PUBLIC == originalComment.optInt(Comment.COMMENT_ANONYMOUS)) {
+ chData.put(Comment.COMMENT_T_AUTHOR_NAME, commenterName);
+ chData.put(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL, avatarQueryService.getAvatarURLByUser(commenter, "48"));
+ } else {
+ chData.put(Comment.COMMENT_T_AUTHOR_NAME, UserExt.ANONYMOUS_USER_NAME);
+ chData.put(Comment.COMMENT_T_AUTHOR_THUMBNAIL_URL, avatarQueryService.getDefaultAvatarURL("48"));
+ }
+
+ chData.put(Common.TIME_AGO, langPropsService.get("justNowLabel"));
+ chData.put(Comment.COMMENT_CREATE_TIME_STR, DateFormatUtils.format(chData.optLong(Comment.COMMENT_CREATE_TIME), "yyyy-MM-dd HH:mm:ss"));
+ String thankTemplate = langPropsService.get("thankConfirmLabel");
+ thankTemplate = thankTemplate.replace("{point}", String.valueOf(Symphonys.POINT_THANK_COMMENT))
+ .replace("{user}", commenterName);
+ chData.put(Comment.COMMENT_T_THANK_LABEL, thankTemplate);
+ String cc = shortLinkQueryService.linkArticle(commentContent);
+ cc = Emotions.toAliases(cc);
+ cc = Emotions.convert(cc);
+ cc = Markdowns.toHTML(cc);
+ cc = Markdowns.clean(cc, "");
+ cc = MP3Players.render(cc);
+ cc = VideoPlayers.render(cc);
+
+ chData.put(Comment.COMMENT_CONTENT, cc);
+ chData.put(Comment.COMMENT_UA, originalComment.optString(Comment.COMMENT_UA));
+
+ ArticleChannel.notifyComment(chData);
+
+ // + Article Heat
+ final JSONObject articleHeat = new JSONObject();
+ articleHeat.put(Article.ARTICLE_T_ID, articleId);
+ articleHeat.put(Common.OPERATION, "+");
+
+ ArticleListChannel.notifyHeat(articleHeat);
+ ArticleChannel.notifyHeat(articleHeat);
+
+ final boolean isDiscussion = originalArticle.optInt(Article.ARTICLE_TYPE) == Article.ARTICLE_TYPE_C_DISCUSSION;
+ final String articleAuthorId = originalArticle.optString(Article.ARTICLE_AUTHOR_ID);
+ final boolean commenterIsArticleAuthor = articleAuthorId.equals(commenterId);
+
+ final Set requisiteAtParticipantsPermissions = new HashSet<>();
+ requisiteAtParticipantsPermissions.add(Permission.PERMISSION_ID_C_COMMON_AT_PARTICIPANTS);
+ final boolean hasAtParticipantPerm = roleQueryService.userHasPermissions(commenterId, requisiteAtParticipantsPermissions);
+
+ if (hasAtParticipantPerm) {
+ // 1. '@participants' Notification
+ if (commentContent.contains("@participants ")) {
+ final List participants = articleQueryService.getArticleLatestParticipants(articleId, Integer.MAX_VALUE);
+ int count = participants.size();
+ if (count < 1) {
+ return;
+ }
+
+ count = 0;
+ for (final JSONObject participant : participants) {
+ final String participantId = participant.optString(Keys.OBJECT_ID);
+ if (participantId.equals(commenterId)) {
+ continue;
+ }
+
+ count++;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, participantId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, commentId);
+ notificationMgmtService.addAtNotification(requestJSONObject);
+ }
+
+ final int sum = count * Pointtransfer.TRANSFER_SUM_C_AT_PARTICIPANTS;
+ if (sum > 0) {
+ pointtransferMgmtService.transfer(commenterId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_AT_PARTICIPANTS, sum, commentId, System.currentTimeMillis(), "");
+ }
+
+ return;
+ }
+ }
+
+ final Set atUserNames = userQueryService.getUserNames(commentContent);
+ atUserNames.remove(commenterName);
+
+ final Set watcherIds = new HashSet<>();
+ final JSONObject followerUsersResult = followQueryService.getArticleWatchers(
+ UserExt.USER_AVATAR_VIEW_MODE_C_ORIGINAL, articleId, 1, Integer.MAX_VALUE);
+
+ final List watcherUsers = (List) followerUsersResult.opt(Keys.RESULTS);
+ for (final JSONObject watcherUser : watcherUsers) {
+ final String watcherUserId = watcherUser.optString(Keys.OBJECT_ID);
+ watcherIds.add(watcherUserId);
+ }
+ watcherIds.remove(articleAuthorId);
+
+ if (commenterIsArticleAuthor && atUserNames.isEmpty() && watcherIds.isEmpty() && StringUtils.isBlank(originalCmtId)) {
+ return;
+ }
+
+ // 2. 'Commented' Notification
+ if (!commenterIsArticleAuthor) {
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, articleAuthorId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, commentId);
+ notificationMgmtService.addCommentedNotification(requestJSONObject);
+ }
+
+ // 3. 'Reply' Notification
+ final Set repliedIds = new HashSet<>();
+ if (StringUtils.isNotBlank(originalCmtId)) {
+ if (!articleAuthorId.equals(originalCmtAuthorId)) {
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, originalCmtAuthorId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, commentId);
+ notificationMgmtService.addReplyNotification(requestJSONObject);
+
+ repliedIds.add(originalCmtAuthorId);
+ }
+ }
+
+ final String articleContent = originalArticle.optString(Article.ARTICLE_CONTENT);
+ final Set articleContentAtUserNames = userQueryService.getUserNames(articleContent);
+
+ final Set requisiteAtUserPermissions = new HashSet<>();
+ requisiteAtUserPermissions.add(Permission.PERMISSION_ID_C_COMMON_AT_USER);
+ final boolean hasAtUserPerm = roleQueryService.userHasPermissions(commenterId, requisiteAtUserPermissions);
+ final Set atIds = new HashSet<>();
+ if (hasAtUserPerm) {
+ // 4. 'At' Notification
+ for (final String atUserName : atUserNames) {
+ if (isDiscussion && !articleContentAtUserNames.contains(atUserName)) {
+ continue;
+ }
+
+ final JSONObject atUser = userQueryService.getUserByName(atUserName);
+ if (atUser.optString(Keys.OBJECT_ID).equals(articleAuthorId)) {
+ continue; // Has notified in step 2
+ }
+
+ final String atUserId = atUser.optString(Keys.OBJECT_ID);
+ if (repliedIds.contains(atUserId)) {
+ continue;
+ }
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, atUserId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, commentId);
+ notificationMgmtService.addAtNotification(requestJSONObject);
+
+ atIds.add(atUserId);
+ }
+ }
+
+ if (64 <= StringUtils.length(commentContent)) {
+ // 5. 'following - article comment' Notification
+ for (final String userId : watcherIds) {
+ final JSONObject watcher = userRepository.get(userId);
+ final String watcherName = watcher.optString(User.USER_NAME);
+ if ((isDiscussion && !articleContentAtUserNames.contains(watcherName)) || commenterName.equals(watcherName)
+ || repliedIds.contains(userId) || atIds.contains(userId)) {
+ continue;
+ }
+
+ // 仅楼主可见回帖不发通知给帖子关注者 https://github.com/b3log/symphony/issues/904
+ if (Comment.COMMENT_VISIBLE_C_AUTHOR == originalComment.optInt(Comment.COMMENT_VISIBLE)) {
+ continue;
+ }
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, userId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, commentId);
+
+ notificationMgmtService.addFollowingArticleCommentNotification(requestJSONObject);
+ }
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Sends the comment notification failed", e);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#ADD_COMMENT_TO_ARTICLE}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.ADD_COMMENT_TO_ARTICLE;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/CommentUpdateNotifier.java b/src/main/java/org/b3log/symphony/event/CommentUpdateNotifier.java
new file mode 100644
index 000000000..a40adea0a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/CommentUpdateNotifier.java
@@ -0,0 +1,140 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.event.AbstractEventListener;
+import org.b3log.latke.event.Event;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Comment;
+import org.b3log.symphony.model.Notification;
+import org.b3log.symphony.model.Permission;
+import org.b3log.symphony.repository.NotificationRepository;
+import org.b3log.symphony.service.NotificationMgmtService;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Sends comment update related notifications.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.2, Nov 17, 2018
+ * @since 2.1.0
+ */
+@Singleton
+public class CommentUpdateNotifier extends AbstractEventListener {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(CommentUpdateNotifier.class);
+
+ /**
+ * Notification repository.
+ */
+ @Inject
+ private NotificationRepository notificationRepository;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ @Override
+ public void action(final Event event) {
+ final JSONObject data = event.getData();
+ LOGGER.log(Level.TRACE, "Processing an event [type={0}, data={1}]", event.getType(), data);
+
+ try {
+ final JSONObject originalArticle = data.getJSONObject(Article.ARTICLE);
+ final JSONObject originalComment = data.getJSONObject(Comment.COMMENT);
+ final String commentId = originalComment.optString(Keys.OBJECT_ID);
+ final String commenterId = originalComment.optString(Comment.COMMENT_AUTHOR_ID);
+ final String commentContent = originalComment.optString(Comment.COMMENT_CONTENT);
+ final JSONObject commenter = userQueryService.getUser(commenterId);
+ final String commenterName = commenter.optString(User.USER_NAME);
+ final Set atUserNames = userQueryService.getUserNames(commentContent);
+ atUserNames.remove(commenterName);
+ final boolean isDiscussion = originalArticle.optInt(Article.ARTICLE_TYPE) == Article.ARTICLE_TYPE_C_DISCUSSION;
+ final String articleAuthorId = originalArticle.optString(Article.ARTICLE_AUTHOR_ID);
+ final String articleContent = originalArticle.optString(Article.ARTICLE_CONTENT);
+ final Set articleContentAtUserNames = userQueryService.getUserNames(articleContent);
+ final Set requisiteAtUserPermissions = new HashSet<>();
+ requisiteAtUserPermissions.add(Permission.PERMISSION_ID_C_COMMON_AT_USER);
+ final boolean hasAtUserPerm = roleQueryService.userHasPermissions(commenterId, requisiteAtUserPermissions);
+ final Set atIds = new HashSet<>();
+ if (hasAtUserPerm) {
+ // 'At' Notification
+ for (final String userName : atUserNames) {
+ if (isDiscussion && !articleContentAtUserNames.contains(userName)) {
+ continue;
+ }
+
+ final JSONObject atUser = userQueryService.getUserByName(userName);
+ if (atUser.optString(Keys.OBJECT_ID).equals(articleAuthorId)) {
+ continue; // Has notified in step 2
+ }
+
+ final String atUserId = atUser.optString(Keys.OBJECT_ID);
+ if (!notificationRepository.hasSentByDataIdAndType(atUserId, commentId, Notification.DATA_TYPE_C_AT)) {
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Notification.NOTIFICATION_USER_ID, atUserId);
+ requestJSONObject.put(Notification.NOTIFICATION_DATA_ID, commentId);
+ notificationMgmtService.addAtNotification(requestJSONObject);
+ }
+
+ atIds.add(atUserId);
+ }
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Sends the comment update notification failed", e);
+ }
+ }
+
+ /**
+ * Gets the event type {@linkplain EventTypes#UPDATE_COMMENT}.
+ *
+ * @return event type
+ */
+ @Override
+ public String getEventType() {
+ return EventTypes.UPDATE_COMMENT;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/event/EventTypes.java b/src/main/java/org/b3log/symphony/event/EventTypes.java
new file mode 100644
index 000000000..149370c91
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/event/EventTypes.java
@@ -0,0 +1,54 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.event;
+
+/**
+ * Event types.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.1, May 6, 2017
+ * @since 0.2.0
+ */
+public final class EventTypes {
+
+ /**
+ * Indicates a add article event.
+ */
+ public static final String ADD_ARTICLE = "Add Article";
+
+ /**
+ * Indicates a update article event.
+ */
+ public static final String UPDATE_ARTICLE = "Update Article";
+
+ /**
+ * Indicates an add comment to article event.
+ */
+ public static final String ADD_COMMENT_TO_ARTICLE = "Add Comment";
+
+ /**
+ * Indicates a update article event.
+ */
+ public static final String UPDATE_COMMENT = "Update Comment";
+
+ /**
+ * Private constructor.
+ */
+ private EventTypes() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Article.java b/src/main/java/org/b3log/symphony/model/Article.java
new file mode 100644
index 000000000..c0dd34997
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Article.java
@@ -0,0 +1,529 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.apache.commons.lang.StringUtils;
+import org.json.JSONObject;
+
+/**
+ * This class defines all article model relevant keys.
+ *
+ * @author Liang Ding
+ * @author qiankunpingtai
+ * @version 1.34.0.3, May 20, 2019
+ * @since 0.2.0
+ */
+public final class Article {
+
+ /**
+ * Article.
+ */
+ public static final String ARTICLE = "article";
+
+ /**
+ * Articles.
+ */
+ public static final String ARTICLES = "articles";
+
+ /**
+ * Key of article title.
+ */
+ public static final String ARTICLE_TITLE = "articleTitle";
+
+ /**
+ * Key of article content.
+ */
+ public static final String ARTICLE_CONTENT = "articleContent";
+
+ /**
+ * Key of article reward content.
+ */
+ public static final String ARTICLE_REWARD_CONTENT = "articleRewardContent";
+
+ /**
+ * Key of article reward point.
+ */
+ public static final String ARTICLE_REWARD_POINT = "articleRewardPoint";
+
+ /**
+ * Key of article tags.
+ */
+ public static final String ARTICLE_TAGS = "articleTags";
+
+ /**
+ * Key of article author id.
+ */
+ public static final String ARTICLE_AUTHOR_ID = "articleAuthorId";
+
+ /**
+ * Key of article comment count.
+ */
+ public static final String ARTICLE_COMMENT_CNT = "articleCommentCount";
+
+ /**
+ * Key of article view count.
+ */
+ public static final String ARTICLE_VIEW_CNT = "articleViewCount";
+
+ /**
+ * Key of article permalink.
+ */
+ public static final String ARTICLE_PERMALINK = "articlePermalink";
+
+ /**
+ * Key of article create time.
+ */
+ public static final String ARTICLE_CREATE_TIME = "articleCreateTime";
+
+ /**
+ * Key of article create time str.
+ */
+ public static final String ARTICLE_CREATE_TIME_STR = "articleCreateTimeStr";
+
+ /**
+ * Key of article update time.
+ */
+ public static final String ARTICLE_UPDATE_TIME = "articleUpdateTime";
+
+ /**
+ * Key of article update time str.
+ */
+ public static final String ARTICLE_UPDATE_TIME_STR = "articleUpdateTimeStr";
+
+ /**
+ * Key of article latest comment time.
+ */
+ public static final String ARTICLE_LATEST_CMT_TIME = "articleLatestCmtTime";
+
+ /**
+ * Key of article latest comment time str.
+ */
+ public static final String ARTICLE_LATEST_CMT_TIME_STR = "articleLatestCmtTimeStr";
+
+ /**
+ * Key of article latest commenter name.
+ */
+ public static final String ARTICLE_LATEST_CMTER_NAME = "articleLatestCmterName";
+
+ /**
+ * Key of article random double value.
+ */
+ public static final String ARTICLE_RANDOM_DOUBLE = "articleRandomDouble";
+
+ /**
+ * Key of article commentable.
+ */
+ public static final String ARTICLE_COMMENTABLE = "articleCommentable";
+
+ /**
+ * Key of article editor type.
+ */
+ public static final String ARTICLE_EDITOR_TYPE = "articleEditorType";
+
+ /**
+ * Key of article status.
+ */
+ public static final String ARTICLE_STATUS = "articleStatus";
+
+ /**
+ * Key of article type.
+ */
+ public static final String ARTICLE_TYPE = "articleType";
+
+ /**
+ * Key of article thank count.
+ */
+ public static final String ARTICLE_THANK_CNT = "articleThankCnt";
+
+ /**
+ * Key of article good count.
+ */
+ public static final String ARTICLE_GOOD_CNT = "articleGoodCnt";
+
+ /**
+ * Key of article bad count.
+ */
+ public static final String ARTICLE_BAD_CNT = "articleBadCnt";
+
+ /**
+ * Key of article collection count.
+ */
+ public static final String ARTICLE_COLLECT_CNT = "articleCollectCnt";
+
+ /**
+ * Key of article watch count.
+ */
+ public static final String ARTICLE_WATCH_CNT = "articleWatchCnt";
+
+ /**
+ * Key of reddit score.
+ */
+ public static final String REDDIT_SCORE = "redditScore";
+
+ /**
+ * Key of article city.
+ */
+ public static final String ARTICLE_CITY = "articleCity";
+
+ /**
+ * Key of article IP.
+ */
+ public static final String ARTICLE_IP = "articleIP";
+
+ /**
+ * Key of article UA.
+ */
+ public static final String ARTICLE_UA = "articleUA";
+
+ /**
+ * Key of article stick.
+ */
+ public static final String ARTICLE_STICK = "articleStick";
+
+ /**
+ * Key of article anonymous.
+ */
+ public static final String ARTICLE_ANONYMOUS = "articleAnonymous";
+
+ /**
+ * Key of article perfect.
+ */
+ public static final String ARTICLE_PERFECT = "articlePerfect";
+
+ /**
+ * Key of article anonymous view.
+ */
+ public static final String ARTICLE_ANONYMOUS_VIEW = "articleAnonymousView";
+
+ /**
+ * Key of article audio URL.
+ */
+ public static final String ARTICLE_AUDIO_URL = "articleAudioURL";
+
+ /**
+ * Key of article qna offer point. https://github.com/b3log/symphony/issues/486
+ */
+ public static final String ARTICLE_QNA_OFFER_POINT = "articleQnAOfferPoint";
+
+ /**
+ * Key of article push order. https://github.com/b3log/symphony/issues/537
+ */
+ public static final String ARTICLE_PUSH_ORDER = "articlePushOrder";
+
+ /**
+ * Key of article image1 URL. https://github.com/b3log/symphony/issues/705
+ */
+ public static final String ARTICLE_IMG1_URL = "articleImg1URL";
+
+ //// Transient ////
+ /**
+ * Key of article revision count.
+ */
+ public static final String ARTICLE_REVISION_COUNT = "articleRevisionCount";
+
+ /**
+ * Key of article latest comment.
+ */
+ public static final String ARTICLE_T_LATEST_CMT = "articleLatestCmt";
+
+ /**
+ * Key of previous article.
+ */
+ public static final String ARTICLE_T_PREVIOUS = "articlePrevious";
+
+ /**
+ * Key of next article.
+ */
+ public static final String ARTICLE_T_NEXT = "articleNext";
+
+ /**
+ * Key of article tag objects.
+ */
+ public static final String ARTICLE_T_TAG_OBJS = "articleTagObjs";
+
+ /**
+ * Key of article vote.
+ */
+ public static final String ARTICLE_T_VOTE = "articleVote";
+
+ /**
+ * Key of article stick flag.
+ */
+ public static final String ARTICLE_T_IS_STICK = "articleIsStick";
+
+ /**
+ * Key of article stick remains.
+ */
+ public static final String ARTICLE_T_STICK_REMAINS = "articleStickRemains";
+
+ /**
+ * Key of article preview content.
+ */
+ public static final String ARTICLE_T_PREVIEW_CONTENT = "articlePreviewContent";
+
+ /**
+ * Key of article thumbnail URL.
+ */
+ public static final String ARTICLE_T_THUMBNAIL_URL = "articleThumbnailURL";
+
+ /**
+ * Key of article view count display format.
+ */
+ public static final String ARTICLE_T_VIEW_CNT_DISPLAY_FORMAT = "articleViewCntDisplayFormat";
+
+ /**
+ * Key of article id.
+ */
+ public static final String ARTICLE_T_ID = "articleId";
+
+ /**
+ * Key of article ids.
+ */
+ public static final String ARTICLE_T_IDS = "articleIds";
+
+ /**
+ * Key of article author.
+ */
+ public static final String ARTICLE_T_AUTHOR = "articleAuthor";
+
+ /**
+ * Key of article author thumbnail URL.
+ */
+ public static final String ARTICLE_T_AUTHOR_THUMBNAIL_URL = "articleAuthorThumbnailURL";
+
+ /**
+ * Key of article author name.
+ */
+ public static final String ARTICLE_T_AUTHOR_NAME = "articleAuthorName";
+
+ /**
+ * Key of article author URL.
+ */
+ public static final String ARTICLE_T_AUTHOR_URL = "articleAuthorURL";
+
+ /**
+ * Key of article author intro.
+ */
+ public static final String ARTICLE_T_AUTHOR_INTRO = "articleAuthorIntro";
+
+ /**
+ * Key of article comments.
+ */
+ public static final String ARTICLE_T_COMMENTS = "articleComments";
+
+ /**
+ * Key of article nice comments.
+ */
+ public static final String ARTICLE_T_NICE_COMMENTS = "articleNiceComments";
+
+ /**
+ * Key of article offered (accepted) comment(answer).
+ */
+ public static final String ARTICLE_T_OFFERED_COMMENT = "articleOfferedComment";
+
+ /**
+ * Key of article participants.
+ */
+ public static final String ARTICLE_T_PARTICIPANTS = "articleParticipants";
+
+ /**
+ * Key of article participant name.
+ */
+ public static final String ARTICLE_T_PARTICIPANT_NAME = "articleParticipantName";
+
+ /**
+ * Key of article participant thumbnail URL.
+ */
+ public static final String ARTICLE_T_PARTICIPANT_THUMBNAIL_URL = "articleParticipantThumbnailURL";
+
+ /**
+ * Key of article participant URL.
+ */
+ public static final String ARTICLE_T_PARTICIPANT_URL = "articleParticipantURL";
+
+ /**
+ * Key of article title with Emoj.
+ */
+ public static final String ARTICLE_T_TITLE_EMOJI = "articleTitleEmoj";
+
+ /**
+ * Key of article title with Emoji unicode.
+ */
+ public static final String ARTICLE_T_TITLE_EMOJI_UNICODE = "articleTitleEmojUnicode";
+
+ /**
+ * Key of article heat.
+ */
+ public static final String ARTICLE_T_HEAT = "articleHeat";
+
+ /**
+ * Key of article ToC.
+ */
+ public static final String ARTICLE_T_TOC = "articleToC";
+
+ /**
+ * Key of article original content.
+ */
+ public static final String ARTICLE_T_ORIGINAL_CONTENT = "articleOriginalContent";
+
+ /**
+ * Key of flag of notifying followers.
+ */
+ public static final String ARTICLE_T_NOTIFY_FOLLOWERS = "articleNotifyFollowers";
+
+ /**
+ * Key of article show in list. https://github.com/b3log/symphony/issues/927
+ */
+ public static final String ARTICLE_SHOW_IN_LIST = "articleShowInList";
+
+ // Anonymous constants
+ /**
+ * Article anonymous - public.
+ */
+ public static final int ARTICLE_ANONYMOUS_C_PUBLIC = 0;
+
+ /**
+ * Article anonymous - anonymous.
+ */
+ public static final int ARTICLE_ANONYMOUS_C_ANONYMOUS = 1;
+
+ // Perfect constants
+ /**
+ * Article perfect - not perfect.
+ */
+ public static final int ARTICLE_PERFECT_C_NOT_PERFECT = 0;
+
+ /**
+ * Article perfect - perfect.
+ */
+ public static final int ARTICLE_PERFECT_C_PERFECT = 1;
+
+ // Anonymous view constants
+ /**
+ * Article anonymous view - use global.
+ */
+ public static final int ARTICLE_ANONYMOUS_VIEW_C_USE_GLOBAL = 0;
+
+ /**
+ * Article anonymous view - not allow.
+ */
+ public static final int ARTICLE_ANONYMOUS_VIEW_C_NOT_ALLOW = 1;
+
+ /**
+ * Article anonymous view - allow.
+ */
+ public static final int ARTICLE_ANONYMOUS_VIEW_C_ALLOW = 2;
+
+ // Status constants
+ /**
+ * Article status - valid.
+ */
+ public static final int ARTICLE_STATUS_C_VALID = 0;
+
+ /**
+ * Article status - invalid.
+ */
+ public static final int ARTICLE_STATUS_C_INVALID = 1;
+
+ /**
+ * Article status - locked.
+ */
+ public static final int ARTICLE_STATUS_C_LOCKED = 2;
+
+ // Type constants
+ /**
+ * Article type - normal.
+ */
+ public static final int ARTICLE_TYPE_C_NORMAL = 0;
+
+ /**
+ * Article type - discussion.
+ */
+ public static final int ARTICLE_TYPE_C_DISCUSSION = 1;
+
+ /**
+ * Article type - city broadcast.
+ */
+ public static final int ARTICLE_TYPE_C_CITY_BROADCAST = 2;
+
+ /**
+ * Article type - thought .
+ */
+ public static final int ARTICLE_TYPE_C_THOUGHT = 3;
+
+ /**
+ * Article type - QnA .
+ */
+ public static final int ARTICLE_TYPE_C_QNA = 5;
+
+ // Show in list constants
+ /**
+ * Article show in list - not.
+ */
+ public static final Integer ARTICLE_SHOW_IN_LIST_C_NOT = 0;
+
+ /**
+ * Article show in list - yes.
+ */
+ public static final Integer ARTICLE_SHOW_IN_LIST_C_YES = 1;
+
+ /**
+ * Checks the specified article1 is different from the specified article2.
+ *
+ * @param a1 the specified article1
+ * @param a2 the specified article2
+ * @return {@code true} if they are different, otherwise returns {@code false}
+ */
+ public static boolean isDifferent(final JSONObject a1, final JSONObject a2) {
+ final String title1 = a1.optString(Article.ARTICLE_TITLE);
+ final String title2 = a2.optString(Article.ARTICLE_TITLE);
+ if (!StringUtils.equalsIgnoreCase(title1, title2)) {
+ return true;
+ }
+
+ final String tags1 = a1.optString(Article.ARTICLE_TAGS);
+ final String tags2 = a2.optString(Article.ARTICLE_TAGS);
+ if (!StringUtils.equalsIgnoreCase(tags1, tags2)) {
+ return true;
+ }
+
+ final String content1 = a1.optString(Article.ARTICLE_CONTENT);
+ final String content2 = a2.optString(Article.ARTICLE_CONTENT);
+ if (!StringUtils.equalsIgnoreCase(content1, content2)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the specified article type is whether invalid.
+ *
+ * @param articleType the specified article type
+ * @return {@code true} if it is invalid, otherwise returns {@code false}
+ */
+ public static boolean isInvalidArticleType(final int articleType) {
+ return articleType < 0 || articleType > Article.ARTICLE_TYPE_C_QNA;
+ }
+
+ /**
+ * Private constructor.
+ */
+ private Article() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Breezemoon.java b/src/main/java/org/b3log/symphony/model/Breezemoon.java
new file mode 100644
index 000000000..8fc941001
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Breezemoon.java
@@ -0,0 +1,107 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all breezemoon model relevant keys. https://github.com/b3log/symphony/issues/507
+ *
+ * @author Liang Ding
+ * @version 1.2.0.0, Jul 20, 2018
+ * @since 2.8.0
+ */
+public final class Breezemoon {
+
+ /**
+ * Breezemoon.
+ */
+ public static final String BREEZEMOON = "breezemoon";
+
+ /**
+ * Breezemoons.
+ */
+ public static final String BREEZEMOONS = "breezemoons";
+
+ /**
+ * Key of breezemoon content.
+ */
+ public static final String BREEZEMOON_CONTENT = "breezemoonContent";
+
+ /**
+ * Key of breezemoon author id.
+ */
+ public static final String BREEZEMOON_AUTHOR_ID = "breezemoonAuthorId";
+
+ /**
+ * Key of breezemoon created at.
+ */
+ public static final String BREEZEMOON_CREATED = "breezemoonCreated";
+
+ /**
+ * Key of breezemoon updated at.
+ */
+ public static final String BREEZEMOON_UPDATED = "breezemoonUpdated";
+
+ /**
+ * Key of breezemoon IP.
+ */
+ public static final String BREEZEMOON_IP = "breezemoonIP";
+
+ /**
+ * Key of breezemoon UA.
+ */
+ public static final String BREEZEMOON_UA = "breezemoonUA";
+
+ /**
+ * Key of breezemoon status.
+ */
+ public static final String BREEZEMOON_STATUS = "breezemoonStatus";
+
+ /**
+ * Key of breezemoon city.
+ */
+ public static final String BREEZEMOON_CITY = "breezemoonCity";
+
+ // Status constants
+ /**
+ * Breezemoon status - valid.
+ */
+ public static final int BREEZEMOON_STATUS_C_VALID = 0;
+
+ /**
+ * Breezemoon status - invalid.
+ */
+ public static final int BREEZEMOON_STATUS_C_INVALID = 1;
+
+ //// Transient ////
+
+ /**
+ * Key of breezemoon author name.
+ */
+ public static final String BREEZEMOON_T_AUTHOR_NAME = "breezemoonAuthorName";
+
+ /**
+ * Key of breezemoon author thumbnail URL.
+ */
+ public static final String BREEZEMOON_T_AUTHOR_THUMBNAIL_URL = "breezemoonAuthorThumbnailURL";
+
+ /**
+ * Key of breezemoon create time.
+ */
+ public static final String BREEZEMOON_T_CREATE_TIME = "breezemoonCreateTime";
+}
+
diff --git a/src/main/java/org/b3log/symphony/model/Character.java b/src/main/java/org/b3log/symphony/model/Character.java
new file mode 100644
index 000000000..1fd383c69
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Character.java
@@ -0,0 +1,100 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.json.JSONObject;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.Set;
+
+/**
+ * This class defines all character model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.0.1.0, Jul 8, 2016
+ * @since 1.4.0
+ */
+public final class Character {
+
+ /**
+ * Character.
+ */
+ public static final String CHARACTER = "character";
+
+ /**
+ * Characters.
+ */
+ public static final String CHARACTERS = "characters";
+
+ /**
+ * Key of character user id.
+ */
+ public static final String CHARACTER_USER_ID = "characterUserId";
+
+ /**
+ * Key of character image.
+ */
+ public static final String CHARACTER_IMG = "characterImg";
+
+ /**
+ * Key of character content.
+ */
+ public static final String CHARACTER_CONTENT = "characterContent";
+
+ /**
+ * Character font.
+ */
+ private static final Font FONT = new Font("宋体", Font.PLAIN, 40);
+
+ /**
+ * Gets a character by the specified character content in the specified characters.
+ *
+ * @param content the specified character content
+ * @param characters the specified characters
+ * @return character, returns {@code null} if not found
+ */
+ public static JSONObject getCharacter(final String content, final Set characters) {
+ for (final JSONObject character : characters) {
+ if (character.optString(CHARACTER_CONTENT).equals(content)) {
+ return character;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an image with the specified content (a character).
+ *
+ * @param content the specified content
+ * @return image
+ */
+ public static BufferedImage createImage(final String content) {
+ final BufferedImage ret = new BufferedImage(500, 500, Transparency.TRANSLUCENT);
+ final Graphics g = ret.getGraphics();
+ g.setClip(0, 0, 50, 50);
+ g.fillRect(0, 0, 50, 50);
+ g.setFont(new Font(null, Font.PLAIN, 40));
+ g.setColor(Color.BLACK);
+ g.drawString(content, 5, 40);
+ g.dispose();
+
+ return ret;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Comment.java b/src/main/java/org/b3log/symphony/model/Comment.java
new file mode 100644
index 000000000..9c3c7a5a0
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Comment.java
@@ -0,0 +1,295 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all comment model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.15.0.1, Jan 30, 2019
+ * @since 0.2.0
+ */
+public final class Comment {
+
+ /**
+ * Comment.
+ */
+ public static final String COMMENT = "comment";
+
+ /**
+ * Comments.
+ */
+ public static final String COMMENTS = "comments";
+
+ /**
+ * Key of comment content.
+ */
+ public static final String COMMENT_CONTENT = "commentContent";
+
+ /**
+ * Key of comment create time.
+ */
+ public static final String COMMENT_CREATE_TIME = "commentCreateTime";
+
+ /**
+ * Key of comment create time str.
+ */
+ public static final String COMMENT_CREATE_TIME_STR = "commentCreateTimeStr";
+
+ /**
+ * Key of comment author id.
+ */
+ public static final String COMMENT_AUTHOR_ID = "commentAuthorId";
+
+ /**
+ * Key of comment on article id.
+ */
+ public static final String COMMENT_ON_ARTICLE_ID = "commentOnArticleId";
+
+ /**
+ * Key of comment sharp URL.
+ */
+ public static final String COMMENT_SHARP_URL = "commentSharpURL";
+
+ /**
+ * Key of original comment id.
+ */
+ public static final String COMMENT_ORIGINAL_COMMENT_ID = "commentOriginalCommentId";
+
+ /**
+ * Key of comment status.
+ */
+ public static final String COMMENT_STATUS = "commentStatus";
+
+ /**
+ * Key of comment IP.
+ */
+ public static final String COMMENT_IP = "commentIP";
+
+ /**
+ * Key of comment UA.
+ */
+ public static final String COMMENT_UA = "commentUA";
+
+ /**
+ * Key of comment anonymous.
+ */
+ public static final String COMMENT_ANONYMOUS = "commentAnonymous";
+
+ /**
+ * Key of comment thank count.
+ */
+ public static final String COMMENT_THANK_CNT = "commentThankCnt";
+
+ /**
+ * Key of comment good count.
+ */
+ public static final String COMMENT_GOOD_CNT = "commentGoodCnt";
+
+ /**
+ * Key of comment bad count.
+ */
+ public static final String COMMENT_BAD_CNT = "commentBadCnt";
+
+ /**
+ * Key of comment score.
+ */
+ public static final String COMMENT_SCORE = "commentScore";
+
+ /**
+ * Key of comment reply count.
+ */
+ public static final String COMMENT_REPLY_CNT = "commentReplyCnt";
+
+ /**
+ * Key of comment audio URL.
+ */
+ public static final String COMMENT_AUDIO_URL = "commentAudioURL";
+
+ /**
+ * Key of comment offered. https://github.com/b3log/symphony/issues/486
+ */
+ public static final String COMMENT_QNA_OFFERED = "commentQnAOffered";
+
+ /**
+ * Key of comment visible.
+ */
+ public static final String COMMENT_VISIBLE = "commentVisible";
+
+ //// Transient ////
+ /**
+ * Key of comment revision count.
+ */
+ public static final String COMMENT_REVISION_COUNT = "commentRevisionCount";
+
+ /**
+ * Key of comment vote.
+ */
+ public static final String COMMENT_T_VOTE = "commentVote";
+
+ /**
+ * Key of commenter.
+ */
+ public static final String COMMENT_T_COMMENTER = "commenter";
+
+ /**
+ * Key of comment author email.
+ */
+ public static final String COMMENT_T_AUTHOR_EMAIL = "commentAuthorEmail";
+
+ /**
+ * Key of comment id.
+ */
+ public static final String COMMENT_T_ID = "commentId";
+
+ /**
+ * Key of comment ids.
+ */
+ public static final String COMMENT_T_IDS = "commentIds";
+
+ /**
+ * Key of comment on symphony article id.
+ */
+ public static final String COMMENT_T_SYMPHONY_ID = "commentSymphonyArticleId";
+
+ /**
+ * Key of comment author thumbnail URL.
+ */
+ public static final String COMMENT_T_AUTHOR_THUMBNAIL_URL = "commentAuthorThumbnailURL";
+
+ /**
+ * Key of comment author name.
+ */
+ public static final String COMMENT_T_AUTHOR_NAME = "commentAuthorName";
+
+ /**
+ * Key of comment author URL.
+ */
+ public static final String COMMENT_T_AUTHOR_URL = "commentAuthorURL";
+
+ /**
+ * Key of comment article title.
+ */
+ public static final String COMMENT_T_ARTICLE_TITLE = "commentArticleTitle";
+
+ /**
+ * Key of comment article type.
+ */
+ public static final String COMMENT_T_ARTICLE_TYPE = "commentArticleType";
+
+ /**
+ * Key of comment article perfect.
+ */
+ public static final String COMMENT_T_ARTICLE_PERFECT = "commentArticlePerfect";
+
+ /**
+ * Key of comment article author name.
+ */
+ public static final String COMMENT_T_ARTICLE_AUTHOR_NAME = "commentArticleAuthorName";
+
+ /**
+ * Key of comment article author URL.
+ */
+ public static final String COMMENT_T_ARTICLE_AUTHOR_URL = "commentArticleAuthorURL";
+
+ /**
+ * Key of comment article author thumbnail URL.
+ */
+ public static final String COMMENT_T_ARTICLE_AUTHOR_THUMBNAIL_URL = "commentArticleAuthorThumbnailURL";
+
+ /**
+ * Key of comment article permalink.
+ */
+ public static final String COMMENT_T_ARTICLE_PERMALINK = "commentArticlePermalink";
+
+ /**
+ * Key of comment thank label.
+ */
+ public static final String COMMENT_T_THANK_LABEL = "commentThankLabel";
+
+ /**
+ * Key of comment nice.
+ */
+ public static final String COMMENT_T_NICE = "commentNice";
+
+ /**
+ * Key of comment replies.
+ */
+ public static final String COMMENT_T_REPLIES = "commentReplies";
+
+ /**
+ * Key of comment original author thumbnail URL.
+ */
+ public static final String COMMENT_T_ORIGINAL_AUTHOR_THUMBNAIL_URL = "commentOriginalAuthorThumbnailURL";
+
+ //// Status constants
+ /**
+ * Comment status - valid.
+ */
+ public static final int COMMENT_STATUS_C_VALID = 0;
+
+ /**
+ * Comment status - invalid.
+ */
+ public static final int COMMENT_STATUS_C_INVALID = 1;
+
+ // Anonymous constants
+ /**
+ * Comment anonymous - public.
+ */
+ public static final int COMMENT_ANONYMOUS_C_PUBLIC = 0;
+
+ /**
+ * Comment anonymous - anonymous.
+ */
+ public static final int COMMENT_ANONYMOUS_C_ANONYMOUS = 1;
+
+ // QnA offered constants
+ /**
+ * Comment offered - not yet.
+ */
+ public static final int COMMENT_QNA_OFFERED_C_NOT = 0;
+
+ /**
+ * Comment offered - yes.
+ */
+ public static final int COMMENT_QNA_OFFERED_C_YES = 1;
+
+ // Visible constants
+ /**
+ * Comment visible - all.
+ */
+ public static final int COMMENT_VISIBLE_C_ALL = 0;
+
+ /**
+ * Comment visible - only author.
+ */
+ public static final int COMMENT_VISIBLE_C_AUTHOR = 1;
+
+ //// Validation constants
+ /**
+ * Max comment content length.
+ */
+ public static final int MAX_COMMENT_CONTENT_LENGTH = 4096;
+
+ /**
+ * Private constructor.
+ */
+ private Comment() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Common.java b/src/main/java/org/b3log/symphony/model/Common.java
new file mode 100644
index 000000000..bcbb5fe3b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Common.java
@@ -0,0 +1,849 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all common model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.73.0.11, Sep 16, 2019
+ * @since 0.2.0
+ */
+public final class Common {
+
+ /**
+ * Key of old article.
+ */
+ public static final String OLD_ARTICLE = "oldArticle";
+
+ /**
+ * Lock.
+ */
+ public static final String LOCK = "lock";
+
+ /**
+ * Key of is single breezemoon page URL.
+ */
+ public static final String IS_SINGLE_BREEZEMOON_URL = "isSingleBreezemoonURL";
+
+ /**
+ * Key of offered.
+ */
+ public static final String OFFERED = "offered";
+
+ /**
+ * Key of query.
+ */
+ public static final String QUERY = "query";
+
+ /**
+ * Key of command.
+ */
+ public static final String CMD = "cmd";
+
+ /**
+ * Key of User-Agent.
+ */
+ public static final String USER_AGENT = "User-Agent";
+
+ /**
+ * Key of add article domains.
+ */
+ public static final String ADD_ARTICLE_DOMAINS = "addArticleDomains";
+
+ /**
+ * Key of all domains.
+ */
+ public static final String ALL_DOMAINS = "allDomains";
+
+ /**
+ * Key of data.
+ */
+ public static final String DATA = "data";
+
+ /**
+ * Key of mans.
+ */
+ public static final String MANS = "mans";
+
+ /**
+ * Key of man HTML.
+ */
+ public static final String MAN_HTML = "manHTML";
+
+ /**
+ * Key of man command.
+ */
+ public static final String MAN_CMD = "manCmd";
+
+ /**
+ * Key of requisite.
+ */
+ public static final String REQUISITE = "requisite";
+
+ /**
+ * Key of requisite message.
+ */
+ public static final String REQUISITE_MSG = "requisiteMsg";
+
+ /**
+ * Key of command.
+ */
+ public static final String COMMAND = "command";
+
+ /**
+ * Key of need captcha.
+ */
+ public static final String NEED_CAPTCHA = "needCaptcha";
+
+ /**
+ * Key of wrong count.
+ */
+ public static final String WRON_COUNT = "wrongCount";
+
+ /**
+ * Key of remember login.
+ */
+ public static final String REMEMBER_LOGIN = "rememberLogin";
+
+ /**
+ * Key of stick articles.
+ */
+ public static final String STICK_ARTICLES = "stickArticles";
+
+ /**
+ * Key of id.
+ */
+ public static final String ID = "id";
+
+ /**
+ * Key of name.
+ */
+ public static final String NAME = "name";
+
+ /**
+ * Key of languages.
+ */
+ public static final String LANGUAGES = "languages";
+
+ /**
+ * Key of timezones.
+ */
+ public static final String TIMEZONES = "timezones";
+
+ /**
+ * Key of selected.
+ */
+ public static final String SELECTED = "selected";
+
+ /**
+ * Key of invited user count.
+ */
+ public static final String INVITED_USER_COUNT = "invitedUserCnt";
+
+ /**
+ * Key of 'sys announce' notifications.
+ */
+ public static final String SYS_ANNOUNCE_NOTIFICATIONS = "sysAnnounceNotifications";
+
+ /**
+ * Key of unread 'sys announce' notifications count.
+ */
+ public static final String UNREAD_SYS_ANNOUNCE_NOTIFICATION_CNT = "unreadSysAnnounceNotificationCnt";
+
+ /**
+ * Key of unread 'new follower' notifications count.
+ */
+ public static final String UNREAD_NEW_FOLLOWER_NOTIFICATION_CNT = "unreadNewFollowerNotificationCnt";
+
+ /**
+ * Key of mouse effects.
+ */
+ public static final String MOUSE_EFFECTS = "mouseEffects";
+
+ /**
+ * Key of is mobile device.
+ */
+ public static final String IS_MOBILE = "isMobile";
+
+ /**
+ * Key of messages.
+ */
+ public static final String MESSAGES = "messages";
+
+ /**
+ * Max length of UA.
+ */
+ public static final int MAX_LENGTH_UA = 255;
+
+ /**
+ * Max length of URL.
+ */
+ public static final int MAX_LENGTH_URL = 255;
+
+ /**
+ * Key of words.
+ */
+ public static final String WORDS = "words";
+
+ /**
+ * Key of word.
+ */
+ public static final String WORD = "word";
+
+ /**
+ * Key of key.
+ */
+ public static final String KEY = "key";
+
+ /**
+ * Key of money.
+ */
+ public static final String MONEY = "money";
+
+ /**
+ * Key of title.
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * Key of WebSocket scheme.
+ */
+ public static final String WEBSOCKET_SCHEME = "wsScheme";
+
+ /**
+ * Key of broadcast point.
+ */
+ public static final String BROADCAST_POINT = "broadcastPoint";
+
+ /**
+ * Key of at.
+ */
+ public static final String AT = "at";
+
+ /**
+ * Key of time ago.
+ */
+ public static final String TIME_AGO = "timeAgo";
+
+ /**
+ * Key of comment time ago.
+ */
+ public static final String CMT_TIME_AGO = "cmtTimeAgo";
+
+ /**
+ * Key of time.
+ */
+ public static final String TIME = "time";
+
+ /**
+ * Key of rewarded count.
+ */
+ public static final String REWARED_COUNT = "rewardedCnt";
+
+ /**
+ * Key of thanked count.
+ */
+ public static final String THANKED_COUNT = "thankedCnt";
+
+ /**
+ * Key of CSRF token.
+ */
+ public static final String CSRF_TOKEN = "csrfToken";
+
+ /**
+ * Key of Lute engine available.
+ */
+ public static final String LUTE_AVAILABLE = "luteAvailable";
+
+ /**
+ * Key of city.
+ */
+ public static final String CITY = "city";
+
+ /**
+ * Key of city found.
+ */
+ public static final String CITY_FOUND = "cityFound";
+
+ /**
+ * Key of country.
+ */
+ public static final String COUNTRY = "country";
+
+ /**
+ * Key of province.
+ */
+ public static final String PROVINCE = "province";
+
+ /**
+ * Key of is reserved.
+ */
+ public static final String IS_RESERVED = "isReserved";
+
+ /**
+ * Key of data id.
+ */
+ public static final String DATA_ID = "dataId";
+
+ /**
+ * Key of memo.
+ */
+ public static final String MEMO = "memo";
+
+ /**
+ * Key of point.
+ */
+ public static final String POINT = "point";
+
+ /**
+ * Key of elapsed.
+ */
+ public static final String ELAPSED = "elapsed";
+
+ /**
+ * Key of closed 1A0001.
+ */
+ public static final String CLOSED_1A0001 = "closed1A0001";
+
+ /**
+ * Key of closed.
+ */
+ public static final String CLOSED = "closed";
+
+ /**
+ * Key of end.
+ */
+ public static final String END = "end";
+
+ /**
+ * Key of hour.
+ */
+ public static final String HOUR = "hour";
+
+ /**
+ * Key of collected.
+ */
+ public static final String COLLECTED = "collected";
+
+ /**
+ * Key of participated.
+ */
+ public static final String PARTICIPATED = "participated";
+
+ /**
+ * Key of to user.
+ */
+ public static final String TO_USER = "toUser";
+
+ /**
+ * Key of amount.
+ */
+ public static final String AMOUNT = "amount";
+
+ /**
+ * Key of small or large.
+ */
+ public static final String SMALL_OR_LARGE = "smallOrLarge";
+
+ /**
+ * Key of is daily checkin.
+ */
+ public static final String IS_DAILY_CHECKIN = "isDailyCheckin";
+
+ /**
+ * Key of mini postfix.
+ */
+ public static final String MINI_POSTFIX = "miniPostfix";
+
+ /**
+ * Value of mini postfix.
+ */
+ public static final String MINI_POSTFIX_VALUE = ".min";
+
+ /**
+ * Static resource version.
+ */
+ public static final String STATIC_RESOURCE_VERSION = "staticResourceVersion";
+
+ /**
+ * Key of go to.
+ */
+ public static final String GOTO = "goto";
+
+ /**
+ * Key of current user.
+ */
+ public static final String CURRENT_USER = "currentUser";
+
+ /**
+ * Key of current.
+ */
+ public static final String CURRENT = "current";
+
+ /**
+ * Key of is logged in.
+ */
+ public static final String IS_LOGGED_IN = "isLoggedIn";
+
+ /**
+ * Key of is admin.
+ */
+ public static final String IS_ADMIN_LOGGED_IN = "isAdminLoggedIn";
+
+ /**
+ * Key of is following.
+ */
+ public static final String IS_FOLLOWING = "isFollowing";
+
+ /**
+ * Key of is watching.
+ */
+ public static final String IS_WATCHING = "isWatching";
+
+ /**
+ * Key of is my article.
+ */
+ public static final String IS_MY_ARTICLE = "isMyArticle";
+
+ /**
+ * Key of logout URL.
+ */
+ public static final String LOGOUT_URL = "logoutURL";
+
+ /**
+ * Key of type.
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * Key of recent articles.
+ */
+ public static final String RECENT_ARTICLES = "recentArticles";
+
+ /**
+ * Key of recent.
+ */
+ public static final String RECENT = "recent";
+
+ /**
+ * Key of qna.
+ */
+ public static final String QNA = "qna";
+
+ /**
+ * Key of watch.
+ */
+ public static final String WATCH = "watch";
+
+ /**
+ * Key of watching articles.
+ */
+ public static final String WATCHING_ARTICLES = "watchingArticles";
+
+ /**
+ * Key of watching breezemoons.
+ */
+ public static final String WATCHING_BREEZEMOONS = "watchingBreezemoons";
+
+ /**
+ * Key of hot.
+ */
+ public static final String HOT = "hot";
+
+ /**
+ * Key of perfect articles.
+ */
+ public static final String PERFECT_ARTICLES = "perfectArticles";
+
+ /**
+ * Key of perfect.
+ */
+ public static final String PERFECT = "perfect";
+
+ /**
+ * Key of side tags.
+ */
+ public static final String SIDE_TAGS = "sideTags";
+
+ /**
+ * Key of side breezemoons.
+ */
+ public static final String SIDE_BREEZEMOONS = "sideBreezemoons";
+
+ /**
+ * Key of navigation trend tags.
+ */
+ public static final String NAV_TREND_TAGS = "navTrendTags";
+
+ /**
+ * Key of new tags.
+ */
+ public static final String NEW_TAGS = "newTags";
+
+ /**
+ * Key of trend tags.
+ */
+ public static final String TREND_TAGS = "trendTags";
+
+ /**
+ * Key of cold tags.
+ */
+ public static final String COLD_TAGS = "coldTags";
+
+ /**
+ * Key of side hot articles.
+ */
+ public static final String SIDE_HOT_ARTICLES = "sideHotArticles";
+
+ /**
+ * Key of side random articles.
+ */
+ public static final String SIDE_RANDOM_ARTICLES = "sideRandomArticles";
+
+ /**
+ * Key of side relevant articles.
+ */
+ public static final String SIDE_RELEVANT_ARTICLES = "sideRelevantArticles";
+
+ /**
+ * Key of side latest comments.
+ */
+ public static final String SIDE_LATEST_CMTS = "sideLatestCmts";
+
+ /**
+ * Key of latest articles.
+ */
+ public static final String LATEST_ARTICLES = "latestArticles";
+
+ /**
+ * Key of index articles.
+ */
+ public static final String INDEX_ARTICLES = "indexArticles";
+
+ /**
+ * Key of index.
+ */
+ public static final String INDEX = "index";
+
+ /**
+ * Key of user home articles.
+ */
+ public static final String USER_HOME_ARTICLES = "userHomeArticles";
+
+ /**
+ * Key of user home comments.
+ */
+ public static final String USER_HOME_COMMENTS = "userHomeComments";
+
+ /**
+ * Key of user home breezemoons.
+ */
+ public static final String USER_HOME_BREEZEMOONS = "userHomeBreezemoons";
+
+ /**
+ * Key of user home following users.
+ */
+ public static final String USER_HOME_FOLLOWING_USERS = "userHomeFollowingUsers";
+
+ /**
+ * Key of user home following tags.
+ */
+ public static final String USER_HOME_FOLLOWING_TAGS = "userHomeFollowingTags";
+
+ /**
+ * Key of user home following articles.
+ */
+ public static final String USER_HOME_FOLLOWING_ARTICLES = "userHomeFollowingArticles";
+
+ /**
+ * Key of user home follower users.
+ */
+ public static final String USER_HOME_FOLLOWER_USERS = "userHomeFollowerUsers";
+
+ /**
+ * Key of user home points.
+ */
+ public static final String USER_HOME_POINTS = "userHomePoints";
+
+ /**
+ * Key of 'point' notifications.
+ */
+ public static final String POINT_NOTIFICATIONS = "pointNotifications";
+
+ /**
+ * Key of unread 'point' notifications count.
+ */
+ public static final String UNREAD_POINT_NOTIFICATION_CNT = "unreadPointNotificationCnt";
+
+ /**
+ * Key of 'commented' notifications.
+ */
+ public static final String COMMENTED_NOTIFICATIONS = "commentedNotifications";
+
+ /**
+ * Key of 'reply' notifications.
+ */
+ public static final String REPLY_NOTIFICATIONS = "replyNotifications";
+
+ /**
+ * Key of unread notifications count.
+ */
+ public static final String UNREAD_NOTIFICATION_CNT = "unreadNotificationCnt";
+
+ /**
+ * Key of unread 'commented' notifications count.
+ */
+ public static final String UNREAD_COMMENTED_NOTIFICATION_CNT = "unreadCommentedNotificationCnt";
+
+ /**
+ * Key of unread 'reply' notifications count.
+ */
+ public static final String UNREAD_REPLY_NOTIFICATION_CNT = "unreadReplyNotificationCnt";
+
+ /**
+ * Key of 'at' notifications.
+ */
+ public static final String AT_NOTIFICATIONS = "atNotifications";
+
+ /**
+ * Key of unread 'at' notifications count.
+ */
+ public static final String UNREAD_AT_NOTIFICATION_CNT = "unreadAtNotificationCnt";
+
+ /**
+ * Key of 'following' notifications.
+ */
+ public static final String FOLLOWING_NOTIFICATIONS = "followingNotifications";
+
+ /**
+ * Key of unread 'broadcast' notifications count.
+ */
+ public static final String UNREAD_BROADCAST_NOTIFICATION_CNT = "unreadBroadcastNotificationCnt";
+
+ /**
+ * Key of 'broadcast' notifications.
+ */
+ public static final String BROADCAST_NOTIFICATIONS = "broadcastNotifications";
+
+ /**
+ * Key of unread 'following' notifications count.
+ */
+ public static final String UNREAD_FOLLOWING_NOTIFICATION_CNT = "unreadFollowingNotificationCnt";
+
+ /**
+ * Key of following user count.
+ */
+ public static final String FOLLOWING_USER_CNT = "followingUserCnt";
+
+ /**
+ * Key of following article count.
+ */
+ public static final String FOLLOWING_ARTICLE_CNT = "followingArticleCnt";
+
+ /**
+ * Key of following tag count.
+ */
+ public static final String FOLLOWING_TAG_CNT = "followingTagCnt";
+
+ /**
+ * Key of author name.
+ */
+ public static final String AUTHOR_NAME = "authorName";
+
+ /**
+ * Key of UA.
+ */
+ public static final String UA = "ua";
+
+ /**
+ * Key of IP.
+ */
+ public static final String IP = "ip";
+
+ /**
+ * Key of content.
+ */
+ public static final String CONTENT = "content";
+
+ /**
+ * Key of thumbnail URL.
+ */
+ public static final String THUMBNAIL_URL = "thumbnailURL";
+
+ /**
+ * Key of URL.
+ */
+ public static final String URL = "url";
+
+ /**
+ * Key of Create time.
+ */
+ public static final String CREATE_TIME = "createTime";
+
+ /**
+ * Key of version.
+ */
+ public static final String VERSION = "version";
+
+ /**
+ * Key of year.
+ */
+ public static final String YEAR = "year";
+
+ /**
+ * Key of site visit statistic code.
+ */
+ public static final String SITE_VISIT_STAT_CODE = "siteVisitStatCode";
+
+ /**
+ * Key of footer bei an hao.
+ */
+ public static final String FOOTER_BEI_AN_HAO = "footerBeiAnHao";
+
+ /**
+ * Key of image max size.
+ */
+ public static final String IMAGE_MAX_SIZE = "imgMaxSize";
+
+ /**
+ * Key of file max size.
+ */
+ public static final String FILE_MAX_SIZE = "fileMaxSize";
+
+ /**
+ * Key of online visitor count.
+ */
+ public static final String ONLINE_VISITOR_CNT = "onlineVisitorCnt";
+
+ /**
+ * Key of online member count.
+ */
+ public static final String ONLINE_MEMBER_CNT = "onlineMemberCnt";
+
+ /**
+ * Key of online chat count.
+ */
+ public static final String ONLINE_CHAT_CNT = "onlineChatCnt";
+
+ /**
+ * Key of article channel count.
+ */
+ public static final String ARTICLE_CHANNEL_CNT = "articleChannelCnt";
+
+ /**
+ * Key of article list channel count.
+ */
+ public static final String ARTICLE_LIST_CHANNEL_CNT = "articleListChannelCnt";
+
+ /**
+ * Key of thread count.
+ */
+ public static final String THREAD_CNT = "threadCnt";
+
+ /**
+ * Key of DB connection count.
+ */
+ public static final String DB_CONN_CNT = "dbConnCnt";
+
+ /**
+ * Key of article comments page size.
+ */
+ public static final String ARTICLE_COMMENTS_PAGE_SIZE = "articleCommentsPageSize";
+
+ /**
+ * Key of weight.
+ */
+ public static final String WEIGHT = "weight";
+
+ /**
+ * Key of viewable.
+ */
+ public static final String DISCUSSION_VIEWABLE = "discussionViewable";
+
+ /**
+ * Key of usernames.
+ */
+ public static final String USER_NAMES = "userNames";
+
+ /**
+ * Key of operation.
+ */
+ public static final String OPERATION = "operation";
+
+ /**
+ * Key of rewarded.
+ */
+ public static final String REWARDED = "rewarded";
+
+ /**
+ * Key of thanked.
+ */
+ public static final String THANKED = "thanked";
+
+ /**
+ * Key of display type.
+ */
+ public static final String DISPLAY_TYPE = "displayType";
+
+ /**
+ * Key of description.
+ */
+ public static final String DESCRIPTION = "description";
+
+ /**
+ * Key of balance.
+ */
+ public static final String BALANCE = "balance";
+
+ /**
+ * Key of referral.
+ */
+ public static final String REFERRAL = "referral";
+
+ /**
+ * Key of top balance users.
+ */
+ public static final String TOP_BALANCE_USERS = "topBalanceUsers";
+
+ /**
+ * Key of top consumption users.
+ */
+ public static final String TOP_CONSUMPTION_USERS = "topConsumptionUsers";
+
+ /**
+ * Key of top checkin users.
+ */
+ public static final String TOP_CHECKIN_USERS = "topCheckinUsers";
+
+ /**
+ * Key of top links.
+ */
+ public static final String TOP_LINKS = "topLinks";
+
+ /**
+ * Key of top.
+ */
+ public static final String TOP = "top";
+
+ /**
+ * Private constructor.
+ */
+ private Common() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Domain.java b/src/main/java/org/b3log/symphony/model/Domain.java
new file mode 100644
index 000000000..c4c063684
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Domain.java
@@ -0,0 +1,147 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines domain model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Mar 30, 2018
+ * @since 1.4.0
+ */
+public final class Domain {
+
+ /**
+ * Domain.
+ */
+ public static final String DOMAIN = "domain";
+
+ /**
+ * Domains.
+ */
+ public static final String DOMAINS = "domains";
+
+ /**
+ * Key of domain title.
+ */
+ public static final String DOMAIN_TITLE = "domainTitle";
+
+ /**
+ * Key of domain URI.
+ */
+ public static final String DOMAIN_URI = "domainURI";
+
+ /**
+ * Key of domain description.
+ */
+ public static final String DOMAIN_DESCRIPTION = "domainDescription";
+
+ /**
+ * Key of domain type.
+ */
+ public static final String DOMAIN_TYPE = "domainType";
+
+ /**
+ * Key of domain sort.
+ */
+ public static final String DOMAIN_SORT = "domainSort";
+
+ /**
+ * Key of domain navigation.
+ */
+ public static final String DOMAIN_NAV = "domainNav";
+
+ /**
+ * Key of domain tag count.
+ */
+ public static final String DOMAIN_TAG_COUNT = "domainTagCnt";
+
+ /**
+ * Key of domain icon path.
+ */
+ public static final String DOMAIN_ICON_PATH = "domainIconPath";
+
+ /**
+ * Key of domain CSS.
+ */
+ public static final String DOMAIN_CSS = "domainCSS";
+
+ /**
+ * Key of domain status.
+ */
+ public static final String DOMAIN_STATUS = "domainStatus";
+
+ /**
+ * Key of domain seo title.
+ */
+ public static final String DOMAIN_SEO_TITLE = "domainSeoTitle";
+
+ /**
+ * Key of domain seo keywords.
+ */
+ public static final String DOMAIN_SEO_KEYWORDS = "domainSeoKeywords";
+
+ /**
+ * Key of domain seo description.
+ */
+ public static final String DOMAIN_SEO_DESC = "domainSeoDesc";
+
+ //// Transient ////
+ /**
+ * Key of domain count.
+ */
+ public static final String DOMAIN_T_COUNT = "domainCnt";
+
+ /**
+ * Key of domain tags.
+ */
+ public static final String DOMAIN_T_TAGS = "domainTags";
+
+ /**
+ * Key of domain id.
+ */
+ public static final String DOMAIN_T_ID = "domainId";
+
+ //// Status constants
+ /**
+ * Domain status - valid.
+ */
+ public static final int DOMAIN_STATUS_C_VALID = 0;
+
+ /**
+ * Domain status - invalid.
+ */
+ public static final int DOMAIN_STATUS_C_INVALID = 1;
+
+ //// Navigation constants
+ /**
+ * Domain navigation - enabled.
+ */
+ public static final int DOMAIN_NAV_C_ENABLED = 0;
+
+ /**
+ * Domain navigation - disabled.
+ */
+ public static final int DOMAIN_NAV_C_DISABLED = 1;
+
+ /**
+ * Private constructor.
+ */
+ private Domain() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Emotion.java b/src/main/java/org/b3log/symphony/model/Emotion.java
new file mode 100644
index 000000000..bbcf540e5
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Emotion.java
@@ -0,0 +1,77 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all emotion model relevant keys.
+ *
+ * @author Zephyr
+ * @version 1.0.0.1, Dec 13, 2016
+ * @since 1.5.0
+ */
+public final class Emotion {
+
+ /**
+ * Emotion.
+ */
+ public static final String EMOTION = "emotion";
+
+ /**
+ * Emotions.
+ */
+ public static final String EMOTIONS = "emotions";
+
+ /**
+ * Key of emotion user id.
+ */
+ public static final String EMOTION_USER_ID = "emotionUserId";
+
+ /**
+ * Key of emotion content.
+ */
+ public static final String EMOTION_CONTENT = "emotionContent";
+
+ /**
+ * Key of emotion sort.
+ */
+ public static final String EMOTION_SORT = "emotionSort";
+
+ /**
+ * Key of emotion type.
+ */
+ public static final String EMOTION_TYPE = "emotionType";
+
+ // Type constants
+ /**
+ * Emotion type - Emoji.
+ */
+ public static final int EMOTION_TYPE_C_EMOJI = 0;
+
+ /**
+ * Key of a short list of all emojis used in setting.
+ */
+ public static final String SHORT_T_LIST = "shortLists";
+
+ /**
+ * Key of end flag of emoji short list.
+ */
+ public static final String EOF_EMOJI = "endOfEmoji";
+
+ private Emotion() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Follow.java b/src/main/java/org/b3log/symphony/model/Follow.java
new file mode 100644
index 000000000..7bc3b3542
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Follow.java
@@ -0,0 +1,80 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all follow model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Jun 3, 2015
+ * @since 0.2.5
+ */
+public final class Follow {
+
+ /**
+ * Follow.
+ */
+ public static final String FOLLOW = "follow";
+
+ /**
+ * Follows.
+ */
+ public static final String FOLLOWS = "follows";
+
+ /**
+ * Key of follower id.
+ */
+ public static final String FOLLOWER_ID = "followerId";
+
+ /**
+ * Key of following id.
+ */
+ public static final String FOLLOWING_ID = "followingId";
+
+ /**
+ * Key of following type.
+ */
+ public static final String FOLLOWING_TYPE = "followingType";
+
+ // Following type constants
+ /**
+ * Following type - user.
+ */
+ public static final int FOLLOWING_TYPE_C_USER = 0;
+
+ /**
+ * Following type - tag.
+ */
+ public static final int FOLLOWING_TYPE_C_TAG = 1;
+
+ /**
+ * Following type - article collect.
+ */
+ public static final int FOLLOWING_TYPE_C_ARTICLE = 2;
+
+ /**
+ * Following type - article watch.
+ */
+ public static final int FOLLOWING_TYPE_C_ARTICLE_WATCH = 3;
+
+ /**
+ * Private constructor.
+ */
+ private Follow() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Invitecode.java b/src/main/java/org/b3log/symphony/model/Invitecode.java
new file mode 100644
index 000000000..59c625d95
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Invitecode.java
@@ -0,0 +1,84 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all invitecode model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Aug 26, 2016
+ * @since 1.4.0
+ */
+public final class Invitecode {
+
+ /**
+ * Invitecode.
+ */
+ public static final String INVITECODE = "invitecode";
+
+ /**
+ * Invitecodes.
+ */
+ public static final String INVITECODES = "invitecodes";
+
+ /**
+ * Key of code.
+ */
+ public static final String CODE = "code";
+
+ /**
+ * Key of generator id.
+ */
+ public static final String GENERATOR_ID = "generatorId";
+
+ /**
+ * Key of user id.
+ */
+ public static final String USER_ID = "userId";
+
+ /**
+ * Key of use time.
+ */
+ public static final String USE_TIME = "useTime";
+
+ /**
+ * Key of status.
+ */
+ public static final String STATUS = "status";
+
+ /**
+ * Key of memo.
+ */
+ public static final String MEMO = "memo";
+
+ // Status constants
+ /**
+ * Status - Used.
+ */
+ public static final int STATUS_C_USED = 0;
+
+ /**
+ * Status - Unused.
+ */
+ public static final int STATUS_C_UNUSED = 1;
+
+ /**
+ * Status - Stop use.
+ */
+ public static final int STATUS_C_STOPUSE = 2;
+}
diff --git a/src/main/java/org/b3log/symphony/model/Link.java b/src/main/java/org/b3log/symphony/model/Link.java
new file mode 100644
index 000000000..d0f527b73
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Link.java
@@ -0,0 +1,160 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all link model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.4.0.0, Oct 1, 2018
+ * @since 1.6.0
+ */
+public final class Link {
+
+ /**
+ * Link.
+ */
+ public static final String LINK = "link";
+
+ /**
+ * Links.
+ */
+ public static final String LINKS = "links";
+
+ /**
+ * Key of link address hash.
+ */
+ public static final String LINK_ADDR_HASH = "linkAddrHash";
+
+ /**
+ * Key of link address.
+ */
+ public static final String LINK_ADDR = "linkAddr";
+
+ /**
+ * Key of link title.
+ */
+ public static final String LINK_TITLE = "linkTitle";
+
+ /**
+ * Key of link submit count.
+ */
+ public static final String LINK_SUBMIT_CNT = "linkSubmitCnt";
+
+ /**
+ * Key of link click count.
+ */
+ public static final String LINK_CLICK_CNT = "linkClickCnt";
+
+ /**
+ * Key of link good count.
+ */
+ public static final String LINK_GOOD_CNT = "linkGoodCnt";
+
+ /**
+ * Key of link bad count.
+ */
+ public static final String LINK_BAD_CNT = "linkBadCnt";
+
+ /**
+ * Key of link Baidu reference count.
+ */
+ public static final String LINK_BAIDU_REF_CNT = "linkBaiduRefCnt";
+
+ /**
+ * Key of link score.
+ */
+ public static final String LINK_SCORE = "linkScore";
+
+ /**
+ * Key of link ping count.
+ */
+ public static final String LINK_PING_CNT = "linkPingCnt";
+
+ /**
+ * Key of link ping error count.
+ */
+ public static final String LINK_PING_ERR_CNT = "linkPingErrCnt";
+
+ /**
+ * Key of link ping time.
+ */
+ public static final String LINK_PING_TIME = "linkPingTime";
+
+ /**
+ * Key of link card HTML.
+ */
+ public static final String LINK_CARD_HTML = "linkCardHTML";
+
+ //// Transient ////
+ /**
+ * Key of link keywords.
+ */
+ public static final String LINK_T_KEYWORDS = "linkKeywords";
+
+ /**
+ * Key of link HTML.
+ */
+ public static final String LINK_T_HTML = "linkHTML";
+
+ /**
+ * Key of link text.
+ */
+ public static final String LINK_T_TEXT = "linkText";
+
+ /**
+ * Key of link count.
+ */
+ public static final String LINK_T_COUNT = "linkCnt";
+
+ /**
+ * Key of link description.
+ */
+ public static final String LINK_T_DESCRIPTION = "linkDescription";
+
+ /**
+ * Key of link image.
+ */
+ public static final String LINK_T_IMAGE = "linkImage";
+
+ /**
+ * Key of link site.
+ */
+ public static final String LINK_T_SITE = "linkSite";
+
+ /**
+ * Key of link site domain.
+ */
+ public static final String LINK_T_SITE_DOMAIN = "linkSiteDomain";
+
+ /**
+ * Key of link site address.
+ */
+ public static final String LINK_T_SITE_ADDR = "linkSiteAddr";
+
+ /**
+ * Key of link site icon.
+ */
+ public static final String LINK_T_SITE_ICON = "linkSiteIcon";
+
+ /**
+ * Private constructor.
+ */
+ private Link() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Liveness.java b/src/main/java/org/b3log/symphony/model/Liveness.java
new file mode 100644
index 000000000..84515ab9b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Liveness.java
@@ -0,0 +1,147 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+/**
+ * This class defines all liveness model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Jun 12, 2018
+ * @since 1.4.0
+ */
+public final class Liveness {
+
+ /**
+ * Liveness.
+ */
+ public static final String LIVENESS = "liveness";
+
+ /**
+ * Key of user id.
+ */
+ public static final String LIVENESS_USER_ID = "livenessUserId";
+
+ /**
+ * Key of liveness date.
+ */
+ public static final String LIVENESS_DATE = "livenessDate";
+
+ /**
+ * Key of liveness point.
+ */
+ public static final String LIVENESS_POINT = "livenessPoint";
+
+ /**
+ * Key of liveness article.
+ */
+ public static final String LIVENESS_ARTICLE = "livenessArticle";
+
+ /**
+ * Key of liveness comment.
+ */
+ public static final String LIVENESS_COMMENT = "livenessComment";
+
+ /**
+ * Key of liveness activity.
+ */
+ public static final String LIVENESS_ACTIVITY = "livenessActivity";
+
+ /**
+ * Key of liveness thank.
+ */
+ public static final String LIVENESS_THANK = "livenessThank";
+
+ /**
+ * Key of liveness vote.
+ */
+ public static final String LIVENESS_VOTE = "livenessVote";
+
+ /**
+ * Key of liveness reward.
+ */
+ public static final String LIVENESS_REWARD = "livenessReward";
+
+ /**
+ * Key of liveness PV.
+ */
+ public static final String LIVENESS_PV = "livenessPV";
+
+ /**
+ * Key of liveness accept answer.
+ */
+ public static final String LIVENESS_ACCEPT_ANSWER = "livenessAcceptAnswer";
+
+ /**
+ * Calculates point of the specified liveness.
+ *
+ * @param liveness the specified liveness
+ * @return point
+ */
+ public static int calcPoint(final JSONObject liveness) {
+ final float activityPer = Symphonys.ACTIVITY_YESTERDAY_REWARD_ACTIVITY_PER;
+ final float articlePer = Symphonys.ACTIVITY_YESTERDAY_REWARD_ARTICLE_PER;
+ final float commentPer = Symphonys.ACTIVITY_YESTERDAY_REWARD_COMMENT_PER;
+ final float pvPer = Symphonys.ACTIVITY_YESTERDAY_REWARD_PV_PER;
+ final float rewardPer = Symphonys.ACTIVITY_YESTERDAY_REWARD_REWARD_PER;
+ final float thankPer = Symphonys.ACTIVITY_YESTERDAY_REWARD_THANK_PER;
+ final float votePer = Symphonys.ACTIVITY_YESTERDAY_REWARD_VOTE_PER;
+ final float acceptAnswerPer = Symphonys.ACTIVITY_YESTERDAY_REWARD_ACCEPT_ANSWER_PER;
+
+ final int activity = liveness.optInt(Liveness.LIVENESS_ACTIVITY);
+ final int article = liveness.optInt(Liveness.LIVENESS_ARTICLE);
+ final int comment = liveness.optInt(Liveness.LIVENESS_COMMENT);
+ int pv = liveness.optInt(Liveness.LIVENESS_PV);
+ if (pv > 50) {
+ pv = 50;
+ }
+ final int reward = liveness.optInt(Liveness.LIVENESS_REWARD);
+ final int thank = liveness.optInt(Liveness.LIVENESS_THANK);
+ int vote = liveness.optInt(Liveness.LIVENESS_VOTE);
+ if (vote > 10) {
+ vote = 10;
+ }
+ final int acceptAnswer = liveness.optInt(Liveness.LIVENESS_ACCEPT_ANSWER);
+
+ final int activityPoint = (int) (activity * activityPer);
+ final int articlePoint = (int) (article * articlePer);
+ final int commentPoint = (int) (comment * commentPer);
+ final int pvPoint = (int) (pv * pvPer);
+ final int rewardPoint = (int) (reward * rewardPer);
+ final int thankPoint = (int) (thank * thankPer);
+ final int votePoint = (int) (vote * votePer);
+ final int acceptAnswerPoint = (int) (acceptAnswer * acceptAnswerPer);
+
+ int ret = activityPoint + articlePoint + commentPoint + pvPoint + rewardPoint + thankPoint + votePoint + acceptAnswerPoint;
+
+ final int max = Symphonys.ACTIVITY_YESTERDAY_REWARD_MAX;
+ if (ret > max) {
+ ret = max;
+ }
+
+ return ret;
+ }
+
+ /**
+ * Private constructor.
+ */
+ private Liveness() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Notification.java b/src/main/java/org/b3log/symphony/model/Notification.java
new file mode 100644
index 000000000..6dde065ae
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Notification.java
@@ -0,0 +1,236 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all notification model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.17.0.0, Jul 15, 2017
+ * @since 0.2.5
+ */
+public final class Notification {
+
+ /**
+ * Notification.
+ */
+ public static final String NOTIFICATION = "notification";
+
+ /**
+ * Notifications.
+ */
+ public static final String NOTIFICATIONS = "notifications";
+
+ /**
+ * Key of user id.
+ */
+ public static final String NOTIFICATION_USER_ID = "userId";
+
+ /**
+ * Key of data id.
+ */
+ public static final String NOTIFICATION_DATA_ID = "dataId";
+
+ /**
+ * Key of data type.
+ */
+ public static final String NOTIFICATION_DATA_TYPE = "dataType";
+
+ /**
+ * Key of has read.
+ */
+ public static final String NOTIFICATION_HAS_READ = "hasRead";
+
+ // Data type constants
+ /**
+ * Data type - article.
+ */
+ public static final int DATA_TYPE_C_ARTICLE = 0;
+
+ /**
+ * Data type - comment.
+ */
+ public static final int DATA_TYPE_C_COMMENT = 1;
+
+ /**
+ * Data type - @.
+ */
+ public static final int DATA_TYPE_C_AT = 2;
+
+ /**
+ * Data type - commented.
+ */
+ public static final int DATA_TYPE_C_COMMENTED = 3;
+
+ /**
+ * Data type - following - user.
+ */
+ public static final int DATA_TYPE_C_FOLLOWING_USER = 4;
+
+ /**
+ * Data type - point charge.
+ */
+ public static final int DATA_TYPE_C_POINT_CHARGE = 5;
+
+ /**
+ * Data type - point transfer.
+ */
+ public static final int DATA_TYPE_C_POINT_TRANSFER = 6;
+
+ /**
+ * Data type - article reward.
+ */
+ public static final int DATA_TYPE_C_POINT_ARTICLE_REWARD = 7;
+
+ /**
+ * Data type - comment reward (thank).
+ */
+ public static final int DATA_TYPE_C_POINT_COMMENT_THANK = 8;
+
+ /**
+ * Data type - broadcast.
+ */
+ public static final int DATA_TYPE_C_BROADCAST = 9;
+
+ /**
+ * Data type - point exchange.
+ */
+ public static final int DATA_TYPE_C_POINT_EXCHANGE = 10;
+
+ /**
+ * Data type - abuse point deduct.
+ */
+ public static final int DATA_TYPE_C_ABUSE_POINT_DEDUCT = 11;
+
+ /**
+ * Data type - article thank.
+ */
+ public static final int DATA_TYPE_C_POINT_ARTICLE_THANK = 12;
+
+ /**
+ * Data type - reply.
+ */
+ public static final int DATA_TYPE_C_REPLY = 13;
+
+ /**
+ * Data type - invitecode used.
+ */
+ public static final int DATA_TYPE_C_INVITECODE_USED = 14;
+
+ /**
+ * Data type - system announcement - article.
+ */
+ public static final int DATA_TYPE_C_SYS_ANNOUNCE_ARTICLE = 15;
+
+ /**
+ * Data type - system announcement - new user.
+ */
+ public static final int DATA_TYPE_C_SYS_ANNOUNCE_NEW_USER = 16;
+
+ /**
+ * Data type - new follower.
+ */
+ public static final int DATA_TYPE_C_NEW_FOLLOWER = 17;
+
+ /**
+ * Data type - invitation link used.
+ */
+ public static final int DATA_TYPE_C_INVITATION_LINK_USED = 18;
+
+ /**
+ * Data type - system announcement - role changed.
+ */
+ public static final int DATA_TYPE_C_SYS_ANNOUNCE_ROLE_CHANGED = 19;
+
+ /**
+ * Data type - following - article update.
+ */
+ public static final int DATA_TYPE_C_FOLLOWING_ARTICLE_UPDATE = 20;
+
+ /**
+ * Data type - following - article comment.
+ */
+ public static final int DATA_TYPE_C_FOLLOWING_ARTICLE_COMMENT = 21;
+
+ /**
+ * Data type - point - perfect article.
+ */
+ public static final int DATA_TYPE_C_POINT_PERFECT_ARTICLE = 22;
+
+ /**
+ * Data type - article new follower.
+ */
+ public static final int DATA_TYPE_C_ARTICLE_NEW_FOLLOWER = 23;
+
+ /**
+ * Data type - article new watcher.
+ */
+ public static final int DATA_TYPE_C_ARTICLE_NEW_WATCHER = 24;
+
+ /**
+ * Data type - comment vote up.
+ */
+ public static final int DATA_TYPE_C_COMMENT_VOTE_UP = 25;
+
+ /**
+ * Data type - comment vote down.
+ */
+ public static final int DATA_TYPE_C_COMMENT_VOTE_DOWN = 26;
+
+ /**
+ * Data type - article vote up.
+ */
+ public static final int DATA_TYPE_C_ARTICLE_VOTE_UP = 27;
+
+ /**
+ * Data type - article vote down.
+ */
+ public static final int DATA_TYPE_C_ARTICLE_VOTE_DOWN = 28;
+
+ /**
+ * Data type - comment accept.
+ */
+ public static final int DATA_TYPE_C_POINT_COMMENT_ACCEPT = 33;
+
+ /**
+ * Data type - report handled.
+ */
+ public static final int DATA_TYPE_C_POINT_REPORT_HANDLED = 36;
+
+ //// Transient ////
+ /**
+ * Key of unread notification count.
+ */
+ public static final String NOTIFICATION_T_UNREAD_COUNT = "unreadNotificationCount";
+
+ /**
+ * Key of at in article.
+ */
+ public static final String NOTIFICATION_T_AT_IN_ARTICLE = "atInArticle";
+
+ /**
+ * Key of is comment.
+ */
+ public static final String NOTIFICATION_T_IS_COMMENT = "isComment";
+
+ /**
+ * Private constructor.
+ */
+ private Notification() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Operation.java b/src/main/java/org/b3log/symphony/model/Operation.java
new file mode 100644
index 000000000..6c82c963c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Operation.java
@@ -0,0 +1,345 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.util.Requests;
+import org.b3log.symphony.util.Headers;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+/**
+ * This class defines all operation model relevant keys. https://github.com/b3log/symphony/issues/786
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Nov 19, 2018
+ * @since 3.4.4
+ */
+public final class Operation {
+
+ /**
+ * Operation.
+ */
+ public static final String OPERATION = "operation";
+
+ /**
+ * Operations.
+ */
+ public static final String OPERATIONS = "operations";
+
+ /**
+ * Key of operation user id.
+ */
+ public static final String OPERATION_USER_ID = "operationUserId";
+
+ /**
+ * Key of operation code.
+ */
+ public static final String OPERATION_CODE = "operationCode";
+
+ /**
+ * Key of operation data id.
+ */
+ public static final String OPERATION_DATA_ID = "operationDataId";
+
+ /**
+ * Key of operation created at.
+ */
+ public static final String OPERATION_CREATED = "operationCreated";
+
+ /**
+ * Key of operation IP.
+ */
+ public static final String OPERATION_IP = "operationIP";
+
+ /**
+ * Key of operation UA.
+ */
+ public static final String OPERATION_UA = "operationUA";
+
+ // Code constants
+ /**
+ * Operation code - make report ignored.
+ */
+ public static final int OPERATION_CODE_C_MAKE_REPORT_IGNORED = 0;
+
+ /**
+ * Operation code - make report handled.
+ */
+ public static final int OPERATION_CODE_C_MAKE_REPORT_HANDLED = 1;
+
+ /**
+ * Operation code - update user address.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_USER_ADDR = 2;
+
+ /**
+ * Operation code - remove role.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_ROLE = 3;
+
+ /**
+ * Operation code - change article email push order.
+ */
+ public static final int OPERATION_CODE_C_CHANGE_ARTICLE_EMAIL_PUSH_ORDER = 4;
+
+ /**
+ * Operation code - update breezemoon.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_BREEZEMOON = 5;
+
+ /**
+ * Operation code - remove breezemoon.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_BREEZEMOON = 6;
+
+ /**
+ * Operation code - push telegram.
+ */
+ public static final int OPERATION_CODE_C_PUSH_TELEGRAM = 7;
+
+ /**
+ * Operation code - withdraw B3T.
+ */
+ public static final int OPERATION_CODE_C_WITHDRAW_B3T = 8;
+
+ /**
+ * Operation code - withdraw B3T.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_UNUSED_TAGS = 9;
+
+ /**
+ * Operation code - add role.
+ */
+ public static final int OPERAIONT_CODE_C_ADD_ROLE = 10;
+
+ /**
+ * Operation code - update role permissions.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_ROLE_PERMS = 11;
+
+ /**
+ * Operation code - add ad pos.
+ */
+ public static final int OPERATION_CODE_C_ADD_AD_POS = 12;
+
+ /**
+ * Operation code - update ad pos.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_AD_POS = 13;
+
+ /**
+ * Operation code - add tag.
+ */
+ public static final int OPERATION_CODE_C_ADD_TAG = 14;
+
+ /**
+ * Operation code - stick article.
+ */
+ public static final int OPERATION_CODE_C_STICK_ARTICLE = 15;
+
+ /**
+ * Operation code - cancel stick article.
+ */
+ public static final int OPERATION_CODE_C_CANCEL_STICK_ARTICLE = 16;
+
+ /**
+ * Operation code - generate invitecodes.
+ */
+ public static final int OPERATION_CODE_C_GENERATE_INVITECODES = 17;
+
+ /**
+ * Operation code - update invitecode.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_INVITECODE = 18;
+
+ /**
+ * Operation code - add article.
+ */
+ public static final int OPERATION_CODE_C_ADD_ARTICLE = 19;
+
+ /**
+ * Operation code - add reserved word.
+ */
+ public static final int OPERATION_CODE_C_ADD_RESERVED_WORD = 20;
+
+ /**
+ * Operation code - update reserved word.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_RESERVED_WORD = 21;
+
+ /**
+ * Operation code - remove reserved word.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_RESERVED_WORD = 22;
+
+ /**
+ * Operation code - remove comment.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_COMMENT = 23;
+
+ /**
+ * Operation code - remove article.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_ARTICLE = 24;
+
+ /**
+ * Operation code - add user.
+ */
+ public static final int OPERATION_CODE_C_ADD_USER = 25;
+
+ /**
+ * Operation code - update user.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_USER = 26;
+
+ /**
+ * Operation code - update user email.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_USER_EMAIL = 27;
+
+ /**
+ * Operation code - update user username.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_USER_NAME = 28;
+
+ /**
+ * Operation code - charge point.
+ */
+ public static final int OPERATION_CODE_C_CHARGE_POINT = 29;
+
+ /**
+ * Operation code - deduct point.
+ */
+ public static final int OPERATION_CODE_C_DEDUCT_POINT = 30;
+
+ /**
+ * Operation code - init point.
+ */
+ public static final int OPERATION_CODE_C_INIT_POINT = 31;
+
+ /**
+ * Operation code - exchange point.
+ */
+ public static final int OPERATION_CODE_C_EXCHANGE_POINT = 32;
+
+ /**
+ * Operation code - update article.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_ARTICLE = 33;
+
+ /**
+ * Operation code - update comment.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_COMMENT = 34;
+
+ /**
+ * Operation code - update misc.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_MISC = 35;
+
+ /**
+ * Operation code - update tag.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_TAG = 36;
+
+ /**
+ * Operation code - update domain.
+ */
+ public static final int OPERATION_CODE_C_UPDATE_DOMAIN = 37;
+
+ /**
+ * Operation code - add domain.
+ */
+ public static final int OPERATION_CODE_C_ADD_DOMAIN = 38;
+
+ /**
+ * Operation code - remove domain.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_DOMAIN = 39;
+
+ /**
+ * Operation code - add domain tag.
+ */
+ public static final int OPERATION_CODE_C_ADD_DOMAIN_TAG = 40;
+
+ /**
+ * Operation code - remove domain tag.
+ */
+ public static final int OPERATION_CODE_C_REMOVE_DOMAIN_TAG = 41;
+
+ /**
+ * Operation code - rebuild algolia tag.
+ */
+ public static final int OPERATION_CODE_C_REBUILD_ALGOLIA_TAG = 42;
+
+ /**
+ * Operation code - rebuild algolia user.
+ */
+ public static final int OPERATION_CODE_C_REBUILD_ALGOLIA_USER = 43;
+
+ /**
+ * Operation code - rebuild articles search index.
+ */
+ public static final int OPERATION_CODE_C_REBUILD_ARTICLES_SEARCH = 44;
+
+ /**
+ * Operation code - rebuild article search index.
+ */
+ public static final int OPERATION_CODE_C_REBUILD_ARTICLE_SEARCH = 45;
+
+ //// Transient ////
+
+ /**
+ * Key of operation user name.
+ */
+ public static final String OPERATION_T_USER_NAME = "operationUserName";
+
+ /**
+ * Key of operation content.
+ */
+ public static final String OPERATION_T_CONTENT = "operationContent";
+
+ /**
+ * Key of operation time.
+ */
+ public static final String OPERATION_T_TIME = "operationTime";
+
+ /**
+ * Creates an operation with the specified request and code,
+ *
+ * @param request the specified request
+ * @param code the specified code
+ * @param dataId the specified data id
+ * @return an operation
+ */
+ public static JSONObject newOperation(final Request request, final int code, final String dataId) {
+ final String ip = Requests.getRemoteAddr(request);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+ final JSONObject user = Sessions.getUser();
+
+ return new JSONObject().
+ put(Operation.OPERATION_USER_ID, user.optString(Keys.OBJECT_ID)).
+ put(Operation.OPERATION_CREATED, System.currentTimeMillis()).
+ put(Operation.OPERATION_CODE, code).
+ put(Operation.OPERATION_DATA_ID, dataId).
+ put(Operation.OPERATION_IP, ip).
+ put(Operation.OPERATION_UA, ua);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Option.java b/src/main/java/org/b3log/symphony/model/Option.java
new file mode 100644
index 000000000..746103545
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Option.java
@@ -0,0 +1,151 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines option model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 2.9.0.0, Jul 27, 2018
+ * @since 0.2.0
+ */
+public final class Option {
+
+ /**
+ * Option.
+ */
+ public static final String OPTION = "option";
+
+ /**
+ * Options.
+ */
+ public static final String OPTIONS = "options";
+
+ /**
+ * Key of option value.
+ */
+ public static final String OPTION_VALUE = "optionValue";
+
+ /**
+ * Key of option category.
+ */
+ public static final String OPTION_CATEGORY = "optionCategory";
+
+ // oId constants
+ /**
+ * Key of member count.
+ */
+ public static final String ID_C_STATISTIC_MEMBER_COUNT = "statisticMemberCount";
+
+ /**
+ * Key of article count.
+ */
+ public static final String ID_C_STATISTIC_ARTICLE_COUNT = "statisticArticleCount";
+
+ /**
+ * Key of domain count.
+ */
+ public static final String ID_C_STATISTIC_DOMAIN_COUNT = "statisticDomainCount";
+
+ /**
+ * Key of tag count.
+ */
+ public static final String ID_C_STATISTIC_TAG_COUNT = "statisticTagCount";
+
+ /**
+ * Key of link count.
+ */
+ public static final String ID_C_STATISTIC_LINK_COUNT = "statisticLinkCount";
+
+ /**
+ * Key of comment count.
+ */
+ public static final String ID_C_STATISTIC_CMT_COUNT = "statisticCmtCount";
+
+ /**
+ * Key of max online visitor count.
+ */
+ public static final String ID_C_STATISTIC_MAX_ONLINE_VISITOR_COUNT = "statisticMaxOnlineVisitorCount";
+
+ /**
+ * Key of allow register.
+ */
+ public static final String ID_C_MISC_ALLOW_REGISTER = "miscAllowRegister";
+
+ /**
+ * Key of allow anonymous view.
+ */
+ public static final String ID_C_MISC_ALLOW_ANONYMOUS_VIEW = "miscAllowAnonymousView";
+
+ /**
+ * Key of allow add article.
+ */
+ public static final String ID_C_MISC_ALLOW_ADD_ARTICLE = "miscAllowAddArticle";
+
+ /**
+ * Key of allow add comment.
+ */
+ public static final String ID_C_MISC_ALLOW_ADD_COMMENT = "miscAllowAddComment";
+
+ /**
+ * Key of language.
+ */
+ public static final String ID_C_MISC_LANGUAGE = "miscLanguage";
+
+ /**
+ * Key of article visit count mode. https://github.com/b3log/symphony/issues/694
+ */
+ public static final String ID_C_MISC_ARTICLE_VISIT_COUNT_MODE = "miscArticleVisitCountMode";
+
+ /**
+ * Key of side full ad.
+ */
+ public static final String ID_C_SIDE_FULL_AD = "adSideFull";
+
+ /**
+ * Key of header banner.
+ */
+ public static final String ID_C_HEADER_BANNER = "headerBanner";
+
+ // Category constants
+ /**
+ * Statistic.
+ */
+ public static final String CATEGORY_C_STATISTIC = "statistic";
+
+ /**
+ * Miscellaneous.
+ */
+ public static final String CATEGORY_C_MISC = "misc";
+
+ /**
+ * Reserved words.
+ */
+ public static final String CATEGORY_C_RESERVED_WORDS = "reserved-words";
+
+ /**
+ * Ad.
+ */
+ public static final String CATEGORY_C_AD = "ad";
+
+ /**
+ * Private constructor.
+ */
+ private Option() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Permission.java b/src/main/java/org/b3log/symphony/model/Permission.java
new file mode 100644
index 000000000..9a45cb5b6
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Permission.java
@@ -0,0 +1,525 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import java.util.Set;
+
+/**
+ * This class defines all permission model relevant keys.
+ *
+ * See #337 for more details.
+ *
+ *
+ * @author Liang Ding
+ * @version 1.12.0.0, Jun 25, 2018
+ * @since 1.8.0
+ */
+public final class Permission {
+
+ /**
+ * Permission.
+ */
+ public static final String PERMISSION = "permission";
+
+ /**
+ * Permissions.
+ */
+ public static final String PERMISSIONS = "permissions";
+
+ /**
+ * Key of permission category.
+ */
+ public static final String PERMISSION_CATEGORY = "permissionCategory";
+
+ /**
+ * Key of permission id.
+ */
+ public static final String PERMISSION_ID = "permissionId";
+
+ //// Transient ////
+ /**
+ * Key of permission categories.
+ */
+ public static final String PERMISSION_T_CATEGORIES = "permissionCategories";
+
+ /**
+ * Key of permission label.
+ */
+ public static final String PERMISSION_T_LABEL = "permissionLabel";
+
+ /**
+ * Key of permission grant.
+ */
+ public static final String PERMISSION_T_GRANT = "permissionGrant";
+
+ // oId constants
+ /**
+ * Id - common - add article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_ADD_ARTICLE = "commonAddArticle";
+
+ /**
+ * Id - common - add article anonymous.
+ */
+ public static final String PERMISSION_ID_C_COMMON_ADD_ARTICLE_ANONYMOUS = "commonAddArticleAnonymous";
+
+ /**
+ * Id - common - update article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_UPDATE_ARTICLE = "commonUpdateArticle";
+
+ /**
+ * Id - common - remove article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_REMOVE_ARTICLE = "commonRemoveArticle";
+
+ /**
+ * Id - common - add comment.
+ */
+ public static final String PERMISSION_ID_C_COMMON_ADD_COMMENT = "commonAddComment";
+
+ /**
+ * Id - common - add breezemoon.
+ */
+ public static final String PERMISSION_ID_C_COMMON_ADD_BREEZEMOON = "commonAddBreezemoon";
+
+ /**
+ * Id - common - update breezemoon.
+ */
+ public static final String PERMISSION_ID_C_COMMON_UPDATE_BREEZEMOON = "commonUpdateBreezemoon";
+
+ /**
+ * Id - common - remove breezemoon.
+ */
+ public static final String PERMISSION_ID_C_COMMON_REMOVE_BREEZEMOON = "commonRemoveBreezemoon";
+
+ /**
+ * Id - common add comment anonymous.
+ */
+ public static final String PERMISSION_ID_C_COMMON_ADD_COMMENT_ANONYMOUS = "commonAddCommentAnonymous";
+
+ /**
+ * Id - common - update comment.
+ */
+ public static final String PERMISSION_ID_C_COMMON_UPDATE_COMMENT = "commonUpdateComment";
+
+ /**
+ * Id - common - remove comment.
+ */
+ public static final String PERMISSION_ID_C_COMMON_REMOVE_COMMENT = "commonRemoveComment";
+
+ /**
+ * Id - common - view comment history.
+ */
+ public static final String PERMISSION_ID_C_COMMON_VIEW_COMMENT_HISTORY = "commonViewCommentHistory";
+
+ /**
+ * Id - common - stick article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_STICK_ARTICLE = "commonStickArticle";
+
+ /**
+ * Id - common - thank article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_THANK_ARTICLE = "commonThankArticle";
+
+ /**
+ * Id - common - good article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_GOOD_ARTICLE = "commonGoodArticle";
+
+ /**
+ * Id - common - bad article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_BAD_ARTICLE = "commonBadArticle";
+
+ /**
+ * Id - common - follow article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_FOLLOW_ARTICLE = "commonFollowArticle";
+
+ /**
+ * Id - common - watch article.
+ */
+ public static final String PERMISSION_ID_C_COMMON_WATCH_ARTICLE = "commonWatchArticle";
+
+ /**
+ * Id - common - view article history.
+ */
+ public static final String PERMISSION_ID_C_COMMON_VIEW_ARTICLE_HISTORY = "commonViewArticleHistory";
+
+ /**
+ * Id - common - thank comment.
+ */
+ public static final String PERMISSION_ID_C_COMMON_THANK_COMMENT = "commonThankComment";
+
+ /**
+ * Id - common - good comment.
+ */
+ public static final String PERMISSION_ID_C_COMMON_GOOD_COMMENT = "commonGoodComment";
+
+ /**
+ * Id - common - bad comment.
+ */
+ public static final String PERMISSION_ID_C_COMMON_BAD_COMMENT = "commonBadComment";
+
+ /**
+ * Id - common - at user.
+ */
+ public static final String PERMISSION_ID_C_COMMON_AT_USER = "commonAtUser";
+
+ /**
+ * Id - common - at participants.
+ */
+ public static final String PERMISSION_ID_C_COMMON_AT_PARTICIPANTS = "commonAtParticipants";
+
+ /**
+ * Id - common - exchange invitation code.
+ */
+ public static final String PERMISSION_ID_C_COMMON_EXCHANGE_INVITATION_CODE = "commonExchangeIC";
+
+ /**
+ * Id - common - use invitation link.
+ */
+ public static final String PERMISSION_ID_C_COMMON_USE_INVITATION_LINK = "commonUseIL";
+
+ /**
+ * Id - user - add user.
+ */
+ public static final String PERMISSION_ID_C_USER_ADD_USER = "userAddUser";
+
+ /**
+ * Id - user - update user basic data.
+ */
+ public static final String PERMISSION_ID_C_USER_UPDATE_USER_BASIC = "userUpdateUserBasic";
+
+ /**
+ * Id - user - update user advanced data.
+ */
+ public static final String PERMISSION_ID_C_USER_UPDATE_USER_ADVANCED = "userUpdateUserAdvanced";
+
+ /**
+ * Id - user - add point.
+ */
+ public static final String PERMISSION_ID_C_USER_ADD_POINT = "userAddPoint";
+
+ /**
+ * Id - user - exchange point.
+ */
+ public static final String PERMISSION_ID_C_USER_EXCHANGE_POINT = "userExchangePoint";
+
+ /**
+ * Id - user - deduct point.
+ */
+ public static final String PERMISSION_ID_C_USER_DEDUCT_POINT = "userDeductPoint";
+
+ /**
+ * Id - article - update article basic.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_UPDATE_ARTICLE_BASIC = "articleUpdateArticleBasic";
+
+ /**
+ * Id - article - stick article.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_STICK_ARTICLE = "articleStickArticle";
+
+ /**
+ * Id - article - cancel stick article.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_CANCEL_STICK_ARTICLE = "articleCancelStickArticle";
+
+ /**
+ * Id - article - rebuild all articles index.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_REINDEX_ARTICLES_INDEX = "articleReindexArticles";
+
+ /**
+ * Id - article - rebuild article index.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_REINDEX_ARTICLE_INDEX = "articleReindexArticle";
+
+ /**
+ * Id - article - add article.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_ADD_ARTICLE = "articleAddArticle";
+
+ /**
+ * Id - article - remove article.
+ */
+ public static final String PERMISSION_ID_C_ARTICLE_REMOVE_ARTICLE = "articleRemoveArticle";
+
+ /**
+ * Id - comment - update comment basic.
+ */
+ public static final String PERMISSION_ID_C_COMMENT_UPDATE_COMMENT_BASIC = "commentUpdateCommentBasic";
+
+ /**
+ * Id - comment - remove comment.
+ */
+ public static final String PERMISSION_ID_C_COMMENT_REMOVE_COMMENT = "commentRemoveComment";
+
+ /**
+ * Id - breezemoon - update breezemoon.
+ */
+ public static final String PERMISSION_ID_C_BREEZEMOON_UPDATE_BREEZEMOON = "breezemoonUpdateBreezemoon";
+
+ /**
+ * Id - breezemoon - remove breezemoon.
+ */
+ public static final String PERMISSION_ID_C_BREEZEMOON_REMOVE_BREEZEMOON = "breezemoonRemoveBreezemoon";
+
+ /**
+ * Id - domain - add domain.
+ */
+ public static final String PERMISSION_ID_C_DOMAIN_ADD_DOMAIN = "domainAddDomain";
+
+ /**
+ * Id - domain - add domain tag.
+ */
+ public static final String PERMISSION_ID_C_DOMAIN_ADD_DOMAIN_TAG = "domainAddDomainTag";
+
+ /**
+ * Id - domain - remove domain tag.
+ */
+ public static final String PERMISSION_ID_C_DOMAIN_REMOVE_DOMAIN_TAG = "domainRemoveDomainTag";
+
+ /**
+ * Id - domain - update domain basic.
+ */
+ public static final String PERMISSION_ID_C_DOMAIN_UPDATE_DOMAIN_BASIC = "domainUpdateDomainBasic";
+
+ /**
+ * Id - domain - remove domain.
+ */
+ public static final String PERMISSION_ID_C_DOMAIN_REMOVE_DOMAIN = "domainRemoveDomain";
+
+ /**
+ * Id - tag - update tag basic.
+ */
+ public static final String PERMISSION_ID_C_TAG_UPDATE_TAG_BASIC = "tagUpdateTagBasic";
+
+ /**
+ * Id - reserved word - add reserved word.
+ */
+ public static final String PERMISSION_ID_C_RW_ADD_RW = "rwAddReservedWord";
+
+ /**
+ * Id - reserved word - update reserved word basic.
+ */
+ public static final String PERMISSION_ID_C_RW_UPDATE_RW_BASIC = "rwUpdateReservedWordBasic";
+
+ /**
+ * Id - reserved word - remove reserved word.
+ */
+ public static final String PERMISSION_ID_C_RW_REMOVE_RW = "rwRemoveReservedWord";
+
+ /**
+ * Id - invitation code - generate ic.
+ */
+ public static final String PERMISSION_ID_C_IC_GEN_IC = "icGenIC";
+
+ /**
+ * Id - invitation code - update ic basic.
+ */
+ public static final String PERMISSION_ID_C_IC_UPDATE_IC_BASIC = "icUpdateICBasic";
+
+ /**
+ * Id - advertise - update side.
+ */
+ public static final String PERMISSION_ID_C_AD_UPDATE_SIDE = "adUpdateADSide";
+
+ /**
+ * Id - advertise - update banner.
+ */
+ public static final String PERMISSION_ID_C_AD_UPDATE_BANNER = "adUpdateBanner";
+
+ /**
+ * Id - misc - allow add article.
+ */
+ public static final String PERMISSION_ID_C_MISC_ALLOW_ADD_ARTICLE = "miscAllowAddArticle";
+
+ /**
+ * Id - misc - allow add comment.
+ */
+ public static final String PERMISSION_ID_C_MISC_ALLOW_ADD_COMMENT = "miscAllowAddComment";
+
+ /**
+ * Id - misc - allow anonymous view.
+ */
+ public static final String PERMISSION_ID_C_MISC_ALLOW_ANONYMOUS_VIEW = "miscAllowAnonymousView";
+
+ /**
+ * Id - misc - change register method.
+ */
+ public static final String PERMISSION_ID_C_MISC_REGISTER_METHOD = "miscRegisterMethod";
+
+ /**
+ * Id - misc - change language.
+ */
+ public static final String PERMISSION_ID_C_MISC_LANGUAGE = "miscLanguage";
+
+ /**
+ * Id - menu - admin.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN = "menuAdmin";
+
+ /**
+ * Id - menu - admin - users.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_USERS = "menuAdminUsers";
+
+ /**
+ * Id - menu - admin - breezemoons.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_BREEZEMOONS = "menuAdminBreezemoons";
+
+ /**
+ * Id - menu - admin - articles.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_ARTICLES = "menuAdminArticles";
+
+ /**
+ * Id - menu - admin - comments.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_COMMENTS = "menuAdminComments";
+
+ /**
+ * Id - menu - admin - domains.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_DOMAINS = "menuAdminDomains";
+
+ /**
+ * Id - menu - admin - tags.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_TAGS = "menuAdminTags";
+
+ /**
+ * Id - menu - admin - reserved words.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_RWS = "menuAdminRWs";
+
+ /**
+ * Id - menu - admin - invitecodes.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_ICS = "menuAdminIcs";
+
+ /**
+ * Id - menu - admin - ad.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_AD = "menuAdminAD";
+
+ /**
+ * Id - menu - admin - roles.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_ROLES = "menuAdminRoles";
+
+ /**
+ * Id - menu - admin - reports.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_REPORTS = "menuAdminReports";
+
+ /**
+ * Id - menu - admin - misc.
+ */
+ public static final String PERMISSION_ID_C_MENU_ADMIN_MISC = "menuAdminMisc";
+
+ // Category constants
+ /**
+ * Category - common function.
+ */
+ public static final int PERMISSION_CATEGORY_C_COMMON = 0;
+
+ /**
+ * Category - user management.
+ */
+ public static final int PERMISSION_CATEGORY_C_USER = 1;
+
+ /**
+ * Category - article management.
+ */
+ public static final int PERMISSION_CATEGORY_C_ARTICLE = 2;
+
+ /**
+ * Category - comment management.
+ */
+ public static final int PERMISSION_CATEGORY_C_COMMENT = 3;
+
+ /**
+ * Category - domain management.
+ */
+ public static final int PERMISSION_CATEGORY_C_DOMAIN = 4;
+
+ /**
+ * Category - tag management.
+ */
+ public static final int PERMISSION_CATEGORY_C_TAG = 5;
+
+ /**
+ * Category - reserved word management.
+ */
+ public static final int PERMISSION_CATEGORY_C_RESERVED_WORD = 6;
+
+ /**
+ * Category - invitecode management.
+ */
+ public static final int PERMISSION_CATEGORY_C_IC = 7;
+
+ /**
+ * Category - advertise management.
+ */
+ public static final int PERMISSION_CATEGORY_C_AD = 8;
+
+ /**
+ * Category - misc management.
+ */
+ public static final int PERMISSION_CATEGORY_C_MISC = 9;
+
+ /**
+ * Category - menu.
+ */
+ public static final int PERMISSION_CATEGORY_C_MENU = 10;
+
+ /**
+ * Category - breezemoon management.
+ */
+ public static final int PERMISSION_CATEGORY_C_BREEZEMOON = 11;
+
+ /**
+ * Private constructor.
+ */
+ private Permission() {
+ }
+
+ /**
+ * Checks whether the specified grant permissions contains the specified requisite permissions.
+ *
+ * @param requisitePermissions the specified requisite permissions
+ * @param grantPermissions the specified grant permissions
+ * @return {@code true} if the specified grant permissions contains the specified requisite permissions, returns
+ * {@code false} otherwise
+ */
+ public static boolean hasPermission(final Set requisitePermissions, final Set grantPermissions) {
+ for (final String requisitePermission : requisitePermissions) {
+ if (!grantPermissions.contains(requisitePermission)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Pointtransfer.java b/src/main/java/org/b3log/symphony/model/Pointtransfer.java
new file mode 100644
index 000000000..b6787f7eb
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Pointtransfer.java
@@ -0,0 +1,399 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.b3log.symphony.util.Symphonys;
+
+/**
+ * This class defines all pointtransfer model relevant keys.
+ *
+ * @author Liang Ding
+ * @author Zephyr
+ * @version 1.27.0.0, Oct 1, 2018
+ * @since 1.3.0
+ */
+public final class Pointtransfer {
+
+ /**
+ * Pointtransfer.
+ */
+ public static final String POINTTRANSFER = "pointtransfer";
+
+ /**
+ * Pointtransfers.
+ */
+ public static final String POINTTRANSFERS = "pointtransfers";
+
+ /**
+ * Key of from user id.
+ */
+ public static final String FROM_ID = "fromId";
+
+ /**
+ * Key of to user id.
+ */
+ public static final String TO_ID = "toId";
+
+ /**
+ * Key of sum.
+ */
+ public static final String SUM = "sum";
+
+ /**
+ * Key of from balance.
+ */
+ public static final String FROM_BALANCE = "fromBalance";
+
+ /**
+ * Key of to balance.
+ */
+ public static final String TO_BALANCE = "toBalance";
+
+ /**
+ * Key of time.
+ */
+ public static final String TIME = "time";
+
+ /**
+ * Key of transfer type.
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * Key of data id.
+ */
+ public static final String DATA_ID = "dataId";
+
+ /**
+ * Key of memo.
+ */
+ public static final String MEMO = "memo";
+
+ // Id constants
+ /**
+ * System.
+ */
+ public static final String ID_C_SYS = "sys";
+
+ // Transfer type and sum constants
+ /**
+ * Transfer type - Initialization Income.
+ */
+ public static final int TRANSFER_TYPE_C_INIT = 0;
+
+ /**
+ * Transfer sum - Initialization.
+ */
+ public static final int TRANSFER_SUM_C_INIT = Symphonys.POINT_INIT;
+
+ /**
+ * Transfer type - Add Article Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_ADD_ARTICLE = 1;
+
+ /**
+ * Transfer sum - Add Article.
+ */
+ public static final int TRANSFER_SUM_C_ADD_ARTICLE = Symphonys.POINT_ADD_ARTICLE;
+
+ /**
+ * Transfer type - Update Article Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_UPDATE_ARTICLE = 2;
+
+ /**
+ * Transfer sum - Update Article.
+ */
+ public static final int TRANSFER_SUM_C_UPDATE_ARTICLE = Symphonys.POINT_UPDATE_ARTICLE;
+
+ /**
+ * Transfer type - Add Comment Income/Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_ADD_COMMENT = 3;
+
+ /**
+ * Transfer sum - Add Comment.
+ */
+ public static final int TRANSFER_SUM_C_ADD_COMMENT = Symphonys.POINT_ADD_COMMENT;
+
+ /**
+ * Transfer sum - Add Self Article Comment.
+ */
+ public static final int TRANSFER_SUM_C_ADD_SELF_ARTICLE_COMMENT = Symphonys.POINT_ADD_SELF_ARTICLE_COMMENT;
+
+ /**
+ * Transfer type - Add Article Reward Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_ADD_ARTICLE_REWARD = 4;
+
+ /**
+ * Transfer sum - Add Article Reward.
+ */
+ public static final int TRANSFER_SUM_C_ADD_ARTICLE_REWARD = Symphonys.POINT_ADD_ARTICLE_REWARD;
+
+ /**
+ * Transfer type - Article Reward Income/Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_ARTICLE_REWARD = 5;
+
+ /**
+ * Transfer type - Invite Register Income.
+ */
+ public static final int TRANSFER_TYPE_C_INVITE_REGISTER = 6;
+
+ /**
+ * Transfer type - Invited Register Income.
+ */
+ public static final int TRANSFER_TYPE_C_INVITED_REGISTER = 7;
+
+ /**
+ * Transfer sum - Invite Register.
+ */
+ public static final int TRANSFER_SUM_C_INVITE_REGISTER = Symphonys.POINT_INVITE_REGISTER;
+
+ /**
+ * Transfer type - Activity - Daily Checkin Income.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_CHECKIN = 8;
+
+ /**
+ * Transfer sum - Activity - Daily Checkin Min.
+ */
+ public static final int TRANSFER_SUM_C_ACTIVITY_CHECKIN_MIN = Symphonys.POINT_ACTIVITY_CHECKIN_MIN;
+
+ /**
+ * Transfer sum - Activity - Daily Checkin Max.
+ */
+ public static final int TRANSFER_SUM_C_ACTIVITY_CHECKIN_MAX = Symphonys.POINT_ACTIVITY_CHECKIN_MAX;
+
+ /**
+ * Transfer type - User Account to User Account.
+ */
+ public static final int TRANSFER_TYPE_C_ACCOUNT2ACCOUNT = 9;
+
+ /**
+ * Transfer type - Activity - 1A0001.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_1A0001 = 10;
+
+ /**
+ * Transfer type - Activity - Daily Checkin Streak Income.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_CHECKIN_STREAK = 11;
+
+ /**
+ * Transfer sum - Activity - Daily Checkin Streak.
+ */
+ public static final int TRANSFER_SUM_C_ACTIVITY_CHECKINT_STREAK = Symphonys.POINT_ACTIVITY_CHECKINT_STREAK;
+
+ /**
+ * Transfer type - Activity - 1A0001 Income.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_1A0001_COLLECT = 12;
+
+ /**
+ * Transfer type - Charge.
+ */
+ public static final int TRANSFER_TYPE_C_CHARGE = 13;
+
+ /**
+ * Transfer type - Comment Reward (Thank) Income/Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_COMMENT_REWARD = 14;
+
+ /**
+ * Transfer type - Add Article Broadcast Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_ADD_ARTICLE_BROADCAST = 15;
+
+ /**
+ * Transfer sum - Add Article.
+ */
+ public static final int TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST = Symphonys.POINT_ADD_ARTICLE_BROADCAST;
+
+ /**
+ * Transfer type - Exchange.
+ */
+ public static final int TRANSFER_TYPE_C_EXCHANGE = 16;
+
+ /**
+ * Transfer type - Abuse Deduct.
+ */
+ public static final int TRANSFER_TYPE_C_ABUSE_DEDUCT = 17;
+
+ /**
+ * Transfer type - Activity - Yesterday Liveness Reward Income.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_YESTERDAY_LIVENESS_REWARD = 18;
+
+ /**
+ * Transfer type - Stick Article.
+ */
+ public static final int TRANSFER_TYPE_C_STICK_ARTICLE = 19;
+
+ /**
+ * Transfer sum - Stick Article.
+ */
+ public static final int TRANSFER_SUM_C_STICK_ARTICLE = Symphonys.POINT_STICK_ARTICLE;
+
+ /**
+ * Transfer type - At Participants.
+ */
+ public static final int TRANSFER_TYPE_C_AT_PARTICIPANTS = 20;
+
+ /**
+ * Transfer sum - At Participants.
+ */
+ public static final int TRANSFER_SUM_C_AT_PARTICIPANTS = Symphonys.POINT_AT_PARTICIPANTS;
+
+ /**
+ * Transfer type - Activity - Character.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_CHARACTER = 21;
+
+ /**
+ * Transfer sum - Activity - Character.
+ */
+ public static final int TRANSFER_SUM_C_ACTIVITY_CHARACTER = Symphonys.POINT_ACTIVITY_CHAR;
+
+ /**
+ * Transfer type - Article Thank Income/Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_ARTICLE_THANK = 22;
+
+ /**
+ * Transfer sum - Article Thank.
+ */
+ public static final int TRANSFER_SUM_C_ARTICLE_THANK = Symphonys.POINT_THANK_ARTICLE;
+
+ /**
+ * Transfer type - Data Export.
+ */
+ public static final int TRANSFER_TYPE_C_DATA_EXPORT = 23;
+
+ /**
+ * Transfer sum - Data Export.
+ */
+ public static final int TRANSFER_SUM_C_DATA_EXPORT = Symphonys.POINT_DATA_EXPORT;
+
+ /**
+ * Transfer type - Buy Invitecode.
+ */
+ public static final int TRANSFER_TYPE_C_BUY_INVITECODE = 24;
+
+ /**
+ * Transfer sum - Buy Invitecode.
+ */
+ public static final int TRANSFER_SUM_C_BUY_INVITECODE = Symphonys.POINT_BUY_INVITECODE;
+
+ /**
+ * Transfer type - Invitecode Used.
+ */
+ public static final int TRANSFER_TYPE_C_INVITECODE_USED = 25;
+
+ /**
+ * Transfer sum - Invitecode Used.
+ */
+ public static final int TRANSFER_SUM_C_INVITECODE_USED = Symphonys.POINT_INVITECODE_USED;
+
+ /**
+ * Transfer type - Activity - Eating Snake.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_EATINGSNAKE = 26;
+
+ /**
+ * Transfer sum - Activity - Eating Snake.
+ */
+ public static final int TRANSFER_SUM_C_ACTIVITY_EATINGSNAKE = Symphonys.POINT_ACTIVITY_EATINGSNAKE;
+
+ /**
+ * Transfer type - Activity - Eating Snake Income.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_EATINGSNAKE_COLLECT = 27;
+
+ /**
+ * Transfer type - Invitation link Used.
+ */
+ public static final int TRANSFER_TYPE_C_INVITATION_LINK_USED = 28; // just a placeholder at present
+
+ /**
+ * Transfer type - Perfect Article.
+ */
+ public static final int TRANSFER_TYPE_C_PERFECT_ARTICLE = 29;
+
+ /**
+ * Transfer sum - Perfect Article.
+ */
+ public static final int TRANSFER_SUM_C_PERFECT_ARTICLE = Symphonys.POINT_PERFECT_ARTICLE;
+
+ /**
+ * Transfer type - Activity - Gobang.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_GOBANG = 30;
+
+ /**
+ * Transfer type - Activity - Gobang Income.
+ */
+ public static final int TRANSFER_TYPE_C_ACTIVITY_GOBANG_COLLECT = 31;
+
+ /**
+ * Transfer sum - Activity - Start Gobang.
+ */
+ public static final int TRANSFER_SUM_C_ACTIVITY_GOBANG_START = Symphonys.POINT_ACTIVITY_GOBANG;
+
+ /**
+ * Transfer type - Update Comment Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_UPDATE_COMMENT = 32;
+
+ /**
+ * Transfer sum - Update Comment.
+ */
+ public static final int TRANSFER_SUM_C_UPDATE_COMMENT = Symphonys.POINT_UPDATE_COMMENT;
+
+ /**
+ * Transfer type - QnA Income/Outcome.
+ */
+ public static final int TRANSFER_TYPE_C_QNA_OFFER = 34;
+
+ /**
+ * Transfer type - Report Handled.
+ */
+ public static final int TRANSFER_TYPE_C_REPORT_HANDLED = 35;
+
+ /**
+ * Transfer sum - Report Handled.
+ */
+ public static final int TRANSFER_SUM_C_REPORT_HANDLED = Symphonys.POINT_REPORT_HANDLED;
+
+ /**
+ * Transfer type - Change Username.
+ */
+ public static final int TRANSFER_TYPE_C_CHANGE_USERNAME = 36;
+
+ /**
+ * Transfer sum - Change Username.
+ */
+ public static final int TRANSFER_SUM_C_CHANGE_USERNAME = Symphonys.POINT_CHANGE_USERNAME;
+
+ /**
+ * Private constructor.
+ */
+ private Pointtransfer() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Referral.java b/src/main/java/org/b3log/symphony/model/Referral.java
new file mode 100644
index 000000000..4bd2beb94
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Referral.java
@@ -0,0 +1,79 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines referral model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Apr 28, 2016
+ * @since 1.4.0
+ */
+public final class Referral {
+
+ /**
+ * Referral.
+ */
+ public static final String REFERRAL = "referral";
+
+ /**
+ * Referrals.
+ */
+ public static final String REFERRALS = "referrals";
+
+ /**
+ * Key of referral user.
+ */
+ public static final String REFERRAL_USER = "referralUser";
+
+ /**
+ * Key of referral data id.
+ */
+ public static final String REFERRAL_DATA_ID = "referralDataId";
+
+ /**
+ * Key of referral type.
+ */
+ public static final String REFERRAL_TYPE = "referralType";
+
+ /**
+ * Key of source IP.
+ */
+ public static final String REFERRAL_IP = "referralIP";
+
+ /**
+ * Key of click.
+ */
+ public static final String REFERRAL_CLICK = "referralClick";
+
+ /**
+ * Key of referral user has point.
+ */
+ public static final String REFERRAL_USER_HAS_POINT = "referralUserHasPoint";
+
+ /**
+ * Key of referral author has point.
+ */
+ public static final String REFERRAL_AUTHOR_HAS_POINT = "referralAuthorHasPoint";
+
+ // Type constants
+ /**
+ * Type - Article.
+ */
+ public static final int REFERRAL_TYPE_C_ARTICLE = 0;
+}
diff --git a/src/main/java/org/b3log/symphony/model/Report.java b/src/main/java/org/b3log/symphony/model/Report.java
new file mode 100644
index 000000000..7f32d4170
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Report.java
@@ -0,0 +1,178 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all report model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.2.0.0, Jul 15, 2018
+ * @since 3.1.0
+ */
+public final class Report {
+
+ /**
+ * Report.
+ */
+ public static final String REPORT = "report";
+
+ /**
+ * Reports.
+ */
+ public static final String REPORTS = "reports";
+
+ /**
+ * Key of report user id.
+ */
+ public static final String REPORT_USER_ID = "reportUserId";
+
+ /**
+ * Key of report data id.
+ */
+ public static final String REPORT_DATA_ID = "reportDataId";
+
+ /**
+ * Key of report data type.
+ */
+ public static final String REPORT_DATA_TYPE = "reportDataType";
+
+ /**
+ * Key of report type.
+ */
+ public static final String REPORT_TYPE = "reportType";
+
+ /**
+ * Key of report memo.
+ */
+ public static final String REPORT_MEMO = "reportMemo";
+
+ /**
+ * Key of report handled.
+ */
+ public static final String REPORT_HANDLED = "reportHandled";
+
+ // Report data type constants
+ /**
+ * Report data type - Article.
+ */
+ public static final int REPORT_DATA_TYPE_C_ARTICLE = 0;
+
+ /**
+ * Report data type - comment.
+ */
+ public static final int REPORT_DATA_TYPE_C_COMMENT = 1;
+
+ /**
+ * Report data type - user.
+ */
+ public static final int REPORT_DATA_TYPE_C_USER = 2;
+
+ // Report type constants
+ /**
+ * Report type - Spam AD.
+ */
+ public static final int REPORT_TYPE_C_SPAM_AD = 0;
+
+ /**
+ * Report type - Pornographic.
+ */
+ public static final int REPORT_TYPE_C_PORNOGRAPHIC = 1;
+
+ /**
+ * Report type - Violation of regulations.
+ */
+ public static final int REPORT_TYPE_C_VIOLATION_OF_REGULATIONS = 2;
+
+ /**
+ * Report type - Allegedly infringing.
+ */
+ public static final int REPORT_TYPE_C_ALLEGEDLY_INFRINGING = 3;
+
+ /**
+ * Report type - Personal attacks.
+ */
+ public static final int REPORT_TYPE_C_PERSONAL_ATTACKS = 4;
+
+ /**
+ * Report type - Posing account.
+ */
+ public static final int REPORT_TYPE_C_POSING_ACCOUNT = 5;
+
+ /**
+ * Report type - Spam AD account.
+ */
+ public static final int REPORT_TYPE_C_SPAM_AD_ACCOUNT = 6;
+
+ /**
+ * Report type - Personal Information Violation.
+ */
+ public static final int REPORT_TYPE_C_PERSONAL_INFO_VIOLATION = 7;
+
+ /**
+ * Report type - Other.
+ */
+ public static final int REPORT_TYPE_C_OTHER = 49;
+
+ // Report handled constants
+ /**
+ * Report handled - not yet.
+ */
+ public static final int REPORT_HANDLED_C_NOT = 0;
+
+ /**
+ * Report handled - yes.
+ */
+ public static final int REPORT_HANDLED_C_YES = 1;
+
+ /**
+ * Report handled - ignored.
+ */
+ public static final int REPORT_HANDLED_C_IGNORED = 2;
+
+ //// Transient ////
+ /**
+ * Key of report user name.
+ */
+ public static final String REPORT_T_USERNAME = "reportUserName";
+
+ /**
+ * Key of report data.
+ */
+ public static final String REPORT_T_DATA = "reportData";
+
+ /**
+ * Key of report data type display string.
+ */
+ public static final String REPORT_T_DATA_TYPE_STR = "reportDataTypeStr";
+
+ /**
+ * Key of report type display string.
+ */
+ public static final String REPORT_T_TYPE_STR = "reportTypeStr";
+
+ /**
+ * Key of report time.
+ */
+ public static final String REPORT_T_TIME = "reportTime";
+
+ /**
+ * Private constructor.
+ */
+ private Report() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Revision.java b/src/main/java/org/b3log/symphony/model/Revision.java
new file mode 100644
index 000000000..d2ebafb8e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Revision.java
@@ -0,0 +1,69 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all revision model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Apr 30, 2017
+ * @since 1.4.0
+ */
+public final class Revision {
+
+ /**
+ * Revision.
+ */
+ public static final String REVISION = "revision";
+
+ /**
+ * Revisions.
+ */
+ public static final String REVISIONS = "revisions";
+
+ /**
+ * Key of revision data type.
+ */
+ public static final String REVISION_DATA_TYPE = "revisionDataType";
+
+ /**
+ * Key of revision data id.
+ */
+ public static final String REVISION_DATA_ID = "revisionDataId";
+
+ /**
+ * Key of revision data.
+ */
+ public static final String REVISION_DATA = "revisionData";
+
+ /**
+ * Key of revision author id.
+ */
+ public static final String REVISION_AUTHOR_ID = "revisionAuthorId";
+
+ // Data type constants
+ /**
+ * Data type - article.
+ */
+ public static final int DATA_TYPE_C_ARTICLE = 0;
+
+ /**
+ * Data type - comment.
+ */
+ public static final int DATA_TYPE_C_COMMENT = 1;
+}
diff --git a/src/main/java/org/b3log/symphony/model/Reward.java b/src/main/java/org/b3log/symphony/model/Reward.java
new file mode 100644
index 000000000..147da8c04
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Reward.java
@@ -0,0 +1,85 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all reward model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.3.0.0, Jun 11, 2018
+ * @since 0.2.5
+ */
+public final class Reward {
+
+ /**
+ * Reward.
+ */
+ public static final String REWARD = "reward";
+
+ /**
+ * Rewards.
+ */
+ public static final String REWARDS = "rewards";
+
+ /**
+ * Key of sender id.
+ */
+ public static final String SENDER_ID = "senderId";
+
+ /**
+ * Key of data id.
+ */
+ public static final String DATA_ID = "dataId";
+
+ /**
+ * Key of type.
+ */
+ public static final String TYPE = "type";
+
+ // Reward type constants
+ /**
+ * Reward type - reward article.
+ */
+ public static final int TYPE_C_ARTICLE = 0;
+
+ /**
+ * Reward type - comment (thank comment).
+ */
+ public static final int TYPE_C_COMMENT = 1;
+
+ /**
+ * Reward type - user.
+ */
+ public static final int TYPE_C_USER = 2;
+
+ /**
+ * Reward type - thank article.
+ */
+ public static final int TYPE_C_THANK_ARTICLE = 3;
+
+ /**
+ * Reward type - accept comment.
+ */
+ public static final int TYPE_C_ACCEPT_COMMENT = 4;
+
+ /**
+ * Private constructor.
+ */
+ private Reward() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Role.java b/src/main/java/org/b3log/symphony/model/Role.java
new file mode 100644
index 000000000..ed78a6587
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Role.java
@@ -0,0 +1,96 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all role model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.2, Dec 14, 2016
+ * @since 1.8.0
+ */
+public final class Role {
+
+ /**
+ * Role.
+ */
+ public static final String ROLE = "role";
+
+ /**
+ * Roles.
+ */
+ public static final String ROLES = "roles";
+
+ /**
+ * Key of role name.
+ */
+ public static final String ROLE_NAME = "roleName";
+
+ /**
+ * Key of role description.
+ */
+ public static final String ROLE_DESCRIPTION = "roleDescription";
+
+ /**
+ * Key of role id.
+ */
+ public static final String ROLE_ID = "roleId";
+
+ //// Transient ////
+ /**
+ * Key of user count.
+ */
+ public static final String ROLE_T_USER_COUNT = "roleUserCount";
+
+ // Role name constants
+ /**
+ * Role name - default.
+ */
+ public static final String ROLE_ID_C_DEFAULT = "defaultRole";
+
+ /**
+ * Role name - admin.
+ */
+ public static final String ROLE_ID_C_ADMIN = "adminRole";
+
+ /**
+ * Role name - leader.
+ */
+ public static final String ROLE_ID_C_LEADER = "leaderRole";
+
+ /**
+ * Role name - regular.
+ */
+ public static final String ROLE_ID_C_REGULAR = "regularRole";
+
+ /**
+ * Role name - member.
+ */
+ public static final String ROLE_ID_C_MEMBER = "memberRole";
+
+ /**
+ * Role name - visitor.
+ */
+ public static final String ROLE_ID_C_VISITOR = "visitorRole";
+
+ /**
+ * Private constructor.
+ */
+ private Role() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Tag.java b/src/main/java/org/b3log/symphony/model/Tag.java
new file mode 100644
index 000000000..e3286abe7
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Tag.java
@@ -0,0 +1,505 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.cache.TagCache;
+import org.b3log.symphony.util.Emotions;
+import org.b3log.symphony.util.Markdowns;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+import org.jsoup.Jsoup;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ * This class defines tag model relevant keys.
+ *
+ * @author Liang Ding
+ * @author Bill Ho
+ * @author Liyuan Li
+ * @version 1.18.0.5, Jun 26, 2019
+ * @since 0.2.0
+ */
+public final class Tag {
+
+ /**
+ * Tag.
+ */
+ public static final String TAG = "tag";
+
+ /**
+ * Tags.
+ */
+ public static final String TAGS = "tags";
+
+ /**
+ * Key of tag title.
+ */
+ public static final String TAG_TITLE = "tagTitle";
+
+ /**
+ * Key of tag URI.
+ */
+ public static final String TAG_URI = "tagURI";
+
+ /**
+ * Key of tag icon path.
+ */
+ public static final String TAG_ICON_PATH = "tagIconPath";
+
+ /**
+ * Key of tag CSS.
+ */
+ public static final String TAG_CSS = "tagCSS";
+
+ /**
+ * Key of tag description.
+ */
+ public static final String TAG_DESCRIPTION = "tagDescription";
+
+ /**
+ * Key of tag reference count.
+ */
+ public static final String TAG_REFERENCE_CNT = "tagReferenceCount";
+
+ /**
+ * Key of tag comment count.
+ */
+ public static final String TAG_COMMENT_CNT = "tagCommentCount";
+
+ /**
+ * Key of tag follower count.
+ */
+ public static final String TAG_FOLLOWER_CNT = "tagFollowerCount";
+
+ /**
+ * Key of link count.
+ */
+ public static final String TAG_LINK_CNT = "tagLinkCount";
+
+ /**
+ * Key of tag status.
+ */
+ public static final String TAG_STATUS = "tagStatus";
+
+ /**
+ * Key of tag good count.
+ */
+ public static final String TAG_GOOD_CNT = "tagGoodCnt";
+
+ /**
+ * Key of tag bad count.
+ */
+ public static final String TAG_BAD_CNT = "tagBadCnt";
+
+ /**
+ * Key of tag seo title.
+ */
+ public static final String TAG_SEO_TITLE = "tagSeoTitle";
+
+ /**
+ * Key of tag seo keywords.
+ */
+ public static final String TAG_SEO_KEYWORDS = "tagSeoKeywords";
+
+ /**
+ * Key of tag seo description.
+ */
+ public static final String TAG_SEO_DESC = "tagSeoDesc";
+
+ /**
+ * Key of tag random double value.
+ */
+ public static final String TAG_RANDOM_DOUBLE = "tagRandomDouble";
+
+ /**
+ * Key of tag ad.
+ */
+ public static final String TAG_AD = "tagAd";
+
+ /**
+ * Key of tag show side ad.
+ */
+ public static final String TAG_SHOW_SIDE_AD = "tagShowSideAd";
+
+ //// Transient ////
+ /**
+ * Key of tag domains.
+ */
+ public static final String TAG_T_DOMAINS = "tagDomains";
+
+ /**
+ * Key of tag count.
+ */
+ public static final String TAG_T_COUNT = "tagCnt";
+
+ /**
+ * Key of tag id.
+ */
+ public static final String TAG_T_ID = "tagId";
+
+ /**
+ * Key of tag description text.
+ */
+ public static final String TAG_T_DESCRIPTION_TEXT = "tagDescriptionText";
+
+ /**
+ * Key of tag create time.
+ */
+ public static final String TAG_T_CREATE_TIME = "tagCreateTime";
+
+ /**
+ * Key of tag creator thumbnail URL.
+ */
+ public static final String TAG_T_CREATOR_THUMBNAIL_URL = "tagCreatorThumbnailURL";
+
+ /**
+ * Key of tag creator name.
+ */
+ public static final String TAG_T_CREATOR_NAME = "tagCreatorName";
+
+ /**
+ * Key of tag participants.
+ */
+ public static final String TAG_T_PARTICIPANTS = "tagParticipants";
+
+ /**
+ * Key of tag participant name.
+ */
+ public static final String TAG_T_PARTICIPANT_NAME = "tagParticipantName";
+
+ /**
+ * Key of tag participant thumbnail URL.
+ */
+ public static final String TAG_T_PARTICIPANT_THUMBNAIL_URL = "tagParticipantThumbnailURL";
+
+ /**
+ * Key of tag participant thumbnail update time.
+ */
+ public static final String TAG_T_PARTICIPANT_THUMBNAIL_UPDATE_TIME = "tagParticipantThumbnailUpdateTime";
+
+ /**
+ * Key of tag participant URL.
+ */
+ public static final String TAG_T_PPARTICIPANT_URL = "tagParticipantURL";
+
+ /**
+ * Key of related tags.
+ */
+ public static final String TAG_T_RELATED_TAGS = "tagRelatedTags";
+
+ /**
+ * Key of tag title lower case.
+ */
+ public static final String TAG_T_TITLE_LOWER_CASE = "tagTitleLowerCase";
+
+ //// Tag type constants
+ /**
+ * Tag type - creator.
+ */
+ public static final int TAG_TYPE_C_CREATOR = 0;
+
+ /**
+ * Tag type - article.
+ */
+ public static final int TAG_TYPE_C_ARTICLE = 1;
+
+ /**
+ * Tag type - user self.
+ */
+ public static final int TAG_TYPE_C_USER_SELF = 2;
+
+ // Status constants
+ /**
+ * Tag status - valid.
+ */
+ public static final int TAG_STATUS_C_VALID = 0;
+
+ /**
+ * Tag status - invalid.
+ */
+ public static final int TAG_STATUS_C_INVALID = 1;
+
+ // Tag title constants
+ /**
+ * Title - Sandbox.
+ */
+ public static final String TAG_TITLE_C_SANDBOX = "Sandbox";
+
+ // Validation
+ /**
+ * Max tag title length.
+ */
+ public static final int MAX_TAG_TITLE_LENGTH = 12;
+
+ /**
+ * Max tag count.
+ */
+ public static final int MAX_TAG_COUNT = 4;
+
+ /**
+ * Tag title pattern string.
+ */
+ public static final String TAG_TITLE_PATTERN_STR = "[\\u4e00-\\u9fa5\\w+\\-.]+";
+
+ /**
+ * Tag title pattern.
+ */
+ public static final Pattern TAG_TITLE_PATTERN = Pattern.compile(TAG_TITLE_PATTERN_STR);
+
+ /**
+ * Normalized tag title mappings.
+ */
+ private static final Map> NORMALIZE_MAPPINGS = new HashMap<>();
+
+ static {
+ NORMALIZE_MAPPINGS.put("JavaScript", new HashSet<>(Arrays.asList("JS")));
+ NORMALIZE_MAPPINGS.put("Elasticsearch", new HashSet<>(Arrays.asList("ES搜索引擎", "ES搜索", "ES")));
+ NORMALIZE_MAPPINGS.put("golang", new HashSet<>(Arrays.asList("Go", "Go语言")));
+ NORMALIZE_MAPPINGS.put("线程", new HashSet<>(Arrays.asList("多线程", "Thread")));
+ NORMALIZE_MAPPINGS.put("Vue.js", new HashSet<>(Arrays.asList("Vue")));
+ NORMALIZE_MAPPINGS.put("Node.js", new HashSet<>(Arrays.asList("NodeJS")));
+ }
+
+ /**
+ * Private constructor.
+ */
+ private Tag() {
+ }
+
+ /**
+ * Uses the head tags.
+ *
+ * @param tagStr the specified tags
+ * @param num the specified used number
+ * @return head tags
+ */
+ public static String useHead(final String tagStr, final int num) {
+ final String[] tags = tagStr.split(",");
+ if (tags.length <= num) {
+ return tagStr;
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < num; i++) {
+ sb.append(tags[i]).append(",");
+ }
+ sb.deleteCharAt(sb.length() - 1);
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the specified tags.
+ *
+ * Trims every tag
+ * Deduplication
+ *
+ *
+ * @param tagStr the specified tags
+ * @return formatted tags string
+ */
+ public static String formatTags(final String tagStr) {
+ final String tagStr1 = tagStr.replaceAll("\\s+", "").replaceAll(",", ",").replaceAll("、", ",").
+ replaceAll(";", ",").replaceAll(";", ",");
+ String[] tagTitles = tagStr1.split(",");
+
+ tagTitles = Strings.trimAll(tagTitles);
+
+ // deduplication
+ final Set titles = new LinkedHashSet<>();
+ for (final String tagTitle : tagTitles) {
+ if (!exists(titles, tagTitle)) {
+ titles.add(tagTitle);
+ }
+ }
+
+ tagTitles = titles.toArray(new String[0]);
+
+ int count = 0;
+ final StringBuilder tagsBuilder = new StringBuilder();
+ for (final String tagTitle : tagTitles) {
+ String title = tagTitle.trim();
+ if (StringUtils.isBlank(title)) {
+ continue;
+ }
+
+ if (containsWhiteListTags(title)) {
+ tagsBuilder.append(title).append(",");
+ count++;
+
+ if (count >= MAX_TAG_COUNT) {
+ break;
+ }
+
+ continue;
+ }
+
+ if (StringUtils.length(title) > MAX_TAG_TITLE_LENGTH) {
+ continue;
+ }
+
+ if (!TAG_TITLE_PATTERN.matcher(title).matches()) {
+ continue;
+ }
+
+ title = normalize(title);
+ tagsBuilder.append(title).append(",");
+ count++;
+
+ if (count >= MAX_TAG_COUNT) {
+ break;
+ }
+ }
+ if (tagsBuilder.length() > 0) {
+ tagsBuilder.deleteCharAt(tagsBuilder.length() - 1);
+ }
+
+ return tagsBuilder.toString();
+ }
+
+ /**
+ * Checks the specified tag string whether contains the reserved tags.
+ *
+ * @param tagStr the specified tag string
+ * @return {@code true} if it contains, returns {@code false} otherwise
+ */
+ public static boolean containsReservedTags(final String tagStr) {
+ for (final String reservedTag : Symphonys.RESERVED_TAGS) {
+ if (StringUtils.containsIgnoreCase(tagStr, reservedTag)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the specified tag string whether contains the white list tags.
+ *
+ * @param tagStr the specified tag string
+ * @return {@code true} if it contains, returns {@code false} otherwise
+ */
+ public static boolean containsWhiteListTags(final String tagStr) {
+ for (final String whiteListTag : Symphonys.WHITE_LIST_TAGS) {
+ if (StringUtils.equalsIgnoreCase(tagStr, whiteListTag)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the specified title exists in the specified title set.
+ *
+ * @param titles the specified title set
+ * @param title the specified title to check
+ * @return {@code true} if exists, returns {@code false} otherwise
+ */
+ private static boolean exists(final Set titles, final String title) {
+ for (final String setTitle : titles) {
+ if (setTitle.equalsIgnoreCase(title)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Normalizes the specified title. For example, Normalizes "JS" to "JavaScript.
+ *
+ * @param title the specified title
+ * @return normalized title
+ */
+ private static String normalize(final String title) {
+ final TagCache cache = BeanManager.getInstance().getReference(TagCache.class);
+ final List iconTags = cache.getIconTags(Integer.MAX_VALUE);
+ Collections.sort(iconTags, (t1, t2) -> {
+ final String u1Title = t1.optString(Tag.TAG_T_TITLE_LOWER_CASE);
+ final String u2Title = t2.optString(Tag.TAG_T_TITLE_LOWER_CASE);
+
+ return u2Title.length() - u1Title.length();
+ });
+
+ for (final JSONObject iconTag : iconTags) {
+ final String iconTagTitle = iconTag.optString(Tag.TAG_TITLE);
+ if (iconTagTitle.length() < 2) {
+ break;
+ }
+
+ if (StringUtils.containsIgnoreCase(title, iconTagTitle)) {
+ return iconTagTitle;
+ }
+ }
+
+ final List allTags = cache.getTags();
+ Collections.sort(allTags, (t1, t2) -> {
+ final String u1Title = t1.optString(Tag.TAG_T_TITLE_LOWER_CASE);
+ final String u2Title = t2.optString(Tag.TAG_T_TITLE_LOWER_CASE);
+
+ return u2Title.length() - u1Title.length();
+ });
+
+ for (final Map.Entry> entry : NORMALIZE_MAPPINGS.entrySet()) {
+ final Set oddTitles = entry.getValue();
+ for (final String oddTitle : oddTitles) {
+ if (StringUtils.equalsIgnoreCase(title, oddTitle)) {
+ return entry.getKey();
+ }
+ }
+ }
+
+ for (final JSONObject tag : allTags) {
+ final String tagTitle = tag.optString(Tag.TAG_TITLE);
+ final String tagURI = tag.optString(Tag.TAG_URI);
+ if (StringUtils.equalsIgnoreCase(title, tagURI) || StringUtils.equalsIgnoreCase(title, tagTitle)) {
+ return tag.optString(Tag.TAG_TITLE);
+ }
+ }
+
+ return title;
+ }
+
+ /**
+ * Fills the description for the specified tag.
+ *
+ * @param tag the specified tag
+ */
+ public static void fillDescription(final JSONObject tag) {
+ if (null == tag) {
+ return;
+ }
+
+ String description = tag.optString(Tag.TAG_DESCRIPTION);
+ String descriptionText = tag.optString(Tag.TAG_TITLE);
+ if (StringUtils.isNotBlank(description)) {
+ description = Emotions.convert(description);
+ description = Markdowns.toHTML(description);
+
+ tag.put(Tag.TAG_DESCRIPTION, description);
+ descriptionText = Jsoup.parse(description).text();
+ }
+ tag.put(Tag.TAG_T_DESCRIPTION_TEXT, descriptionText);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/UserExt.java b/src/main/java/org/b3log/symphony/model/UserExt.java
new file mode 100644
index 000000000..2d4438b59
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/UserExt.java
@@ -0,0 +1,691 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+/**
+ * This class defines ext of user model relevant keys.
+ *
+ * @author Liang Ding
+ * @author Bill Ho
+ * @version 2.16.0.1, Jan 10, 2019
+ * @see org.b3log.latke.model.User
+ * @since 0.2.0
+ */
+public final class UserExt {
+
+ /**
+ * Key of index redirect URL.
+ */
+ public static final String USER_INDEX_REDIRECT_URL = "userIndexRedirectURL";
+
+ /**
+ * Key of status of using forward page.
+ */
+ public static final String USER_FORWARD_PAGE_STATUS = "userForwardPageStatus";
+
+ /**
+ * Key of user guide step.
+ */
+ public static final String USER_GUIDE_STEP = "userGuideStep";
+
+ /**
+ * Key of user language.
+ */
+ public static final String USER_LANGUAGE = "userLanguage";
+
+ /**
+ * Key of user timezone.
+ */
+ public static final String USER_TIMEZONE = "userTimezone";
+
+ /**
+ * Key of user keyboard shortcuts status.
+ */
+ public static final String USER_KEYBOARD_SHORTCUTS_STATUS = "userKeyboardShortcutsStatus";
+
+ /**
+ * Key of user subscription mail status.
+ */
+ public static final String USER_SUB_MAIL_STATUS = "userSubMailStatus";
+
+ /**
+ * Key of user auto watch article after reply status.
+ */
+ public static final String USER_REPLY_WATCH_ARTICLE_STATUS = "userReplyWatchArticleStatus";
+
+ /**
+ * Key of user subscription mail send time.
+ */
+ public static final String USER_SUB_MAIL_SEND_TIME = "userSubMailSendTime";
+
+ /**
+ * Key of user avatar view mode.
+ */
+ public static final String USER_AVATAR_VIEW_MODE = "userAvatarViewMode";
+
+ /**
+ * Key of user list page size.
+ */
+ public static final String USER_LIST_PAGE_SIZE = "userListPageSize";
+
+ /**
+ * Key of user list view mode.
+ */
+ public static final String USER_LIST_VIEW_MODE = "userListViewMode";
+
+ /**
+ * Key of user breezemoons status
+ */
+ public static final String USER_BREEZEMOON_STATUS = "userBreezemoonStatus";
+
+ /**
+ * Key of user point status.
+ */
+ public static final String USER_POINT_STATUS = "userPointStatus";
+
+ /**
+ * Key of user follower status.
+ */
+ public static final String USER_FOLLOWER_STATUS = "userFollowerStatus";
+
+ /**
+ * Key of user following article status.
+ */
+ public static final String USER_FOLLOWING_ARTICLE_STATUS = "userFollowingArticleStatus";
+
+ /**
+ * Key of user watching article status.
+ */
+ public static final String USER_WATCHING_ARTICLE_STATUS = "userWatchingArticleStatus";
+
+ /**
+ * Key of user following tag status.
+ */
+ public static final String USER_FOLLOWING_TAG_STATUS = "userFollowingTagStatus";
+
+ /**
+ * Key of user following user status.
+ */
+ public static final String USER_FOLLOWING_USER_STATUS = "userFollowingUserStatus";
+
+ /**
+ * Key of user comment status.
+ */
+ public static final String USER_COMMENT_STATUS = "userCommentStatus";
+
+ /**
+ * Key of user article status.
+ */
+ public static final String USER_ARTICLE_STATUS = "userArticleStatus";
+
+ /**
+ * Key of user online status.
+ */
+ public static final String USER_ONLINE_STATUS = "userOnlineStatus";
+
+ /**
+ * Key of user User-Agent status.
+ */
+ public static final String USER_UA_STATUS = "userUAStatus";
+
+ /**
+ * Key of user notify status.
+ */
+ public static final String USER_NOTIFY_STATUS = "userNotifyStatus";
+
+ /**
+ * Key of user nickname.
+ */
+ public static final String USER_NICKNAME = "userNickname";
+
+ /**
+ * Key of user comment view mode.
+ */
+ public static final String USER_COMMENT_VIEW_MODE = "userCommentViewMode";
+
+ /**
+ * Key of user geo status.
+ */
+ public static final String USER_GEO_STATUS = "userGeoStatus";
+
+ /**
+ * Key of user update time.
+ */
+ public static final String USER_UPDATE_TIME = "userUpdateTime";
+
+ /**
+ * Key of user city.
+ */
+ public static final String USER_CITY = "userCity";
+
+ /**
+ * Key of user country.
+ */
+ public static final String USER_COUNTRY = "userCountry";
+
+ /**
+ * Key of user province.
+ */
+ public static final String USER_PROVINCE = "userProvince";
+
+ /**
+ * Key of user skin.
+ */
+ public static final String USER_SKIN = "userSkin";
+
+ /**
+ * Key of mobile user skin.
+ */
+ public static final String USER_MOBILE_SKIN = "userMobileSkin";
+
+ /**
+ * Key of user checkin time.
+ */
+ public static final String USER_CHECKIN_TIME = "userCheckinTime";
+
+ /**
+ * Key of user longest checkin streak start.
+ */
+ public static final String USER_LONGEST_CHECKIN_STREAK_START = "userLongestCheckinStreakStart";
+
+ /**
+ * Key of user longest checkin streak end.
+ */
+ public static final String USER_LONGEST_CHECKIN_STREAK_END = "userLongestCheckinStreakEnd";
+
+ /**
+ * Key of user current checkin streak start.
+ */
+ public static final String USER_CURRENT_CHECKIN_STREAK_START = "userCurrentCheckinStreakStart";
+
+ /**
+ * Key of user current checkin streak start end.
+ */
+ public static final String USER_CURRENT_CHECKIN_STREAK_END = "userCurrentCheckinStreakEnd";
+
+ /**
+ * Key of user longest checkin streak.
+ */
+ public static final String USER_LONGEST_CHECKIN_STREAK = "userLongestCheckinStreak";
+
+ /**
+ * Key of user current checkin streak.
+ */
+ public static final String USER_CURRENT_CHECKIN_STREAK = "userCurrentCheckinStreak";
+
+ /**
+ * Key of user article count.
+ */
+ public static final String USER_ARTICLE_COUNT = "userArticleCount";
+
+ /**
+ * Key of user comment count.
+ */
+ public static final String USER_COMMENT_COUNT = "userCommentCount";
+
+ /**
+ * Key of new tag count.
+ */
+ public static final String USER_TAG_COUNT = "userTagCount";
+
+ /**
+ * Key of user status.
+ */
+ public static final String USER_STATUS = "userStatus";
+
+ /**
+ * Key of user point.
+ */
+ public static final String USER_POINT = "userPoint";
+
+ /**
+ * Key of user used point.
+ */
+ public static final String USER_USED_POINT = "userUsedPoint";
+
+ /**
+ * Key of user join point rank.
+ */
+ public static final String USER_JOIN_POINT_RANK = "userJoinPointRank";
+
+ /**
+ * Key of user join used point rank.
+ */
+ public static final String USER_JOIN_USED_POINT_RANK = "userJoinUsedPointRank";
+
+ /**
+ * Key of user tags.
+ */
+ public static final String USER_TAGS = "userTags";
+
+ /**
+ * Key of user QQ.
+ */
+ public static final String USER_QQ = "userQQ";
+
+ /**
+ * Key of user number.
+ */
+ public static final String USER_NO = "userNo";
+
+ /**
+ * Key of user intro.
+ */
+ public static final String USER_INTRO = "userIntro";
+
+ /**
+ * Key of user avatar type.
+ */
+ public static final String USER_AVATAR_TYPE = "userAvatarType";
+
+ /**
+ * Key of user avatar URL.
+ */
+ public static final String USER_AVATAR_URL = "userAvatarURL";
+
+ /**
+ * Key of online flag.
+ */
+ public static final String USER_ONLINE_FLAG = "userOnlineFlag";
+
+ /**
+ * Key of latest post article time.
+ */
+ public static final String USER_LATEST_ARTICLE_TIME = "userLatestArticleTime";
+
+ /**
+ * Key of latest comment time.
+ */
+ public static final String USER_LATEST_CMT_TIME = "userLatestCmtTime";
+
+ /**
+ * Key of latest login time.
+ */
+ public static final String USER_LATEST_LOGIN_TIME = "userLatestLoginTime";
+
+ /**
+ * Key of latest login IP.
+ */
+ public static final String USER_LATEST_LOGIN_IP = "userLatestLoginIP";
+
+ /**
+ * Key of app role.
+ */
+ public static final String USER_APP_ROLE = "userAppRole";
+
+ //// Transient ////
+ /**
+ * Key of user create time.
+ */
+ public static final String USER_T_CREATE_TIME = "userCreateTime";
+
+ /**
+ * Key of user point in Hex.
+ */
+ public static final String USER_T_POINT_HEX = "userPointHex";
+
+ /**
+ * Key of user point in Color Code.
+ */
+ public static final String USER_T_POINT_CC = "userPointCC";
+
+ /**
+ * Key of user name lower case.
+ */
+ public static final String USER_T_NAME_LOWER_CASE = "userNameLowerCase";
+
+ /**
+ * Key of user id.
+ */
+ public static final String USER_T_ID = "userId";
+
+ //// User subscription mail status constants
+ /**
+ * User subscription mail status - enabled.
+ */
+ public static final int USER_SUB_MAIL_STATUS_ENABLED = 0;
+
+ /**
+ * User subscription mail status - disabled.
+ */
+ public static final int USER_SUB_MAIL_STATUS_DISABLED = 1;
+
+ //// User guide step constants
+ /**
+ * User guide step - finish.
+ */
+ public static final int USER_GUIDE_STEP_FIN = 0;
+
+ /**
+ * User guide step - upload avatar.
+ */
+ public static final int USER_GUIDE_STEP_UPLOAD_AVATAR = 1;
+
+ /**
+ * User guide step - follow tags.
+ */
+ public static final int USER_GUIDE_STEP_FOLLOW_TAGS = 2;
+
+ /**
+ * User guide step - follow users.
+ */
+ public static final int USER_GUIDE_STEP_FOLLOW_USERS = 3;
+
+ /**
+ * User guide step - star project.
+ */
+ public static final int USER_GUIDE_STEP_STAR_PROJECT = 4;
+
+ //// Email constant
+ /**
+ * Builtin email suffix.
+ */
+ public static final String USER_BUILTIN_EMAIL_SUFFIX = "@sym.b3log.org";
+
+ //// Community Bot constants
+ /**
+ * Bot name.
+ */
+ public static final String COM_BOT_NAME = "ComBot";
+
+ /**
+ * Bot email.
+ */
+ public static final String COM_BOT_EMAIL = "combot" + USER_BUILTIN_EMAIL_SUFFIX;
+
+ //// Null user
+ /**
+ * Null user name.
+ */
+ public static final String NULL_USER_NAME = "_";
+
+ //// Anonymous user.
+ /**
+ * Anonymous user name.
+ */
+ public static final String ANONYMOUS_USER_NAME = "someone";
+
+ /**
+ * Anonymous user id.
+ */
+ public static final String ANONYMOUS_USER_ID = "0";
+
+ //// Status constants
+ /**
+ * User status - valid.
+ */
+ public static final int USER_STATUS_C_VALID = 0;
+
+ /**
+ * User status - invalid.
+ */
+ public static final int USER_STATUS_C_INVALID = 1;
+
+ /**
+ * User status - registered but not verified.
+ */
+ public static final int USER_STATUS_C_NOT_VERIFIED = 2;
+
+ /**
+ * User status - invalid login.
+ */
+ public static final int USER_STATUS_C_INVALID_LOGIN = 3;
+
+ /**
+ * User status - deactivated.
+ */
+ public static final int USER_STATUS_C_DEACTIVATED = 4;
+
+ //// Join join XXX rank constants
+ /**
+ * User join XXX rank - join.
+ */
+ public static final int USER_JOIN_XXX_C_JOIN = 0;
+
+ /**
+ * User join XXX rank - not join.
+ */
+ public static final int USER_JOIN_XXX_C_NOT_JOIN = 1;
+
+ //// User XXX status constants
+ /**
+ * User XXX (notify/point/follower/following article/watching article/following tag/following user/comment/article/breezemoon) status - public.
+ */
+ public static final int USER_XXX_STATUS_C_PUBLIC = 0;
+
+ /**
+ * User XXX (notify/point/follower/following article/watching article/following tag/following user/comment/article/breezemoon) status - private.
+ */
+ public static final int USER_XXX_STATUS_C_PRIVATE = 1;
+
+ /**
+ * User XXX status - enabled.
+ */
+ public static final int USER_XXX_STATUS_C_ENABLED = 0;
+
+ /**
+ * User XXX status - disabled.
+ */
+ public static final int USER_XXX_STATUS_C_DISABLED = 1;
+
+ //// Avatar View Mode constants
+ /**
+ * User avatar view mode - original.
+ */
+ public static final int USER_AVATAR_VIEW_MODE_C_ORIGINAL = 0;
+
+ /**
+ * User avatar view mode - static.
+ */
+ public static final int USER_AVATAR_VIEW_MODE_C_STATIC = 1;
+
+ //// Comment View Mode constants
+ /**
+ * User comment view mode - traditional.
+ */
+ public static final int USER_COMMENT_VIEW_MODE_C_TRADITIONAL = 0;
+
+ /**
+ * User comment view mode - real time.
+ */
+ public static final int USER_COMMENT_VIEW_MODE_C_REALTIME = 1;
+
+ //// Geo Status constants
+ /**
+ * User geo status - public.
+ */
+ public static final int USER_GEO_STATUS_C_PUBLIC = 0;
+
+ /**
+ * User geo status - private.
+ */
+ public static final int USER_GEO_STATUS_C_PRIVATE = 1;
+
+ //// Avatar type constants
+ /**
+ * User avatar type - Gravatar.
+ *
+ * @deprecated only upload allowed since 1.3.0
+ */
+ public static final int USER_AVATAR_TYPE_C_GRAVATAR = 0;
+
+ /**
+ * User avatar type - External Link.
+ *
+ * @deprecated only upload allowed since 1.3.0
+ */
+ public static final int USER_AVATAR_TYPE_C_EXTERNAL_LINK = 1;
+
+ /**
+ * User avatar type - Upload.
+ */
+ public static final int USER_AVATAR_TYPE_C_UPLOAD = 2;
+
+ //// App role constants
+ /**
+ * User app role - Hacker.
+ */
+ public static final int USER_APP_ROLE_C_HACKER = 0;
+
+ /**
+ * User app role - Painter.
+ */
+ public static final int USER_APP_ROLE_C_PAINTER = 1;
+
+ //// List view mode constants
+ /**
+ * List view mode - Only title.
+ */
+ public static final int USER_LIST_VIEW_MODE_TITLE = 0;
+
+ /**
+ * List view mode - Title & Abstract.
+ */
+ public static final int USER_LIST_VIEW_MODE_TITLE_ABSTRACT = 1;
+
+ /**
+ * Private constructor.
+ */
+ private UserExt() {
+ }
+
+ /**
+ * Gets color code of the specified point.
+ *
+ * @param point the specified point
+ * @return color code
+ */
+ public static String toCCString(final int point) {
+ final String hex = Integer.toHexString(point);
+
+ if (1 == hex.length()) {
+ return hex + hex + hex + hex + hex + hex;
+ }
+
+ if (2 == hex.length()) {
+ final String a1 = hex.substring(0, 1);
+ final String a2 = hex.substring(1);
+
+ return a1 + a1 + a1 + a2 + a2 + a2;
+ }
+
+ if (3 == hex.length()) {
+ final String a1 = hex.substring(0, 1);
+ final String a2 = hex.substring(1, 2);
+ final String a3 = hex.substring(2);
+
+ return a1 + a1 + a2 + a2 + a3 + a3;
+ }
+
+ if (4 == hex.length()) {
+ final String a1 = hex.substring(0, 1);
+ final String a2 = hex.substring(1, 2);
+ final String a3 = hex.substring(2, 3);
+ final String a4 = hex.substring(3);
+
+ return a1 + a2 + a3 + a4 + a3 + a4;
+ }
+
+ if (5 == hex.length()) {
+ final String a1 = hex.substring(0, 1);
+ final String a2 = hex.substring(1, 2);
+ final String a3 = hex.substring(2, 3);
+ final String a4 = hex.substring(3, 4);
+ final String a5 = hex.substring(4);
+
+ return a1 + a2 + a3 + a4 + a5 + a5;
+ }
+
+ if (6 == hex.length()) {
+ return hex;
+ }
+
+ return hex.substring(0, 6);
+ }
+
+
+ /**
+ * Checks the specified email whether in a valid mail domain.
+ *
+ * @param email the specified email
+ * @return {@code true} if it is, returns {@code false} otherwise
+ */
+ public static boolean isValidMailDomain(final String email) {
+ final String whitelistMailDomains = Symphonys.MAIL_DOMAINS;
+ if (StringUtils.isBlank(whitelistMailDomains)) {
+ return true;
+ }
+
+ final String domain = StringUtils.substringAfter(email, "@");
+
+ return StringUtils.containsIgnoreCase(whitelistMailDomains, domain);
+ }
+
+ /**
+ * Checks the specified user name whether is a reserved user name.
+ *
+ * @param userName the specified username
+ * @return {@code true} if it is, returns {@code false} otherwise
+ */
+ public static boolean isReservedUserName(final String userName) {
+ for (final String reservedUserName : Symphonys.RESERVED_USER_NAMES) {
+ if (StringUtils.equalsIgnoreCase(userName, reservedUserName)) {
+ return true;
+ }
+ }
+
+ return StringUtils.containsIgnoreCase(userName, UserExt.ANONYMOUS_USER_NAME);
+
+ }
+
+ /**
+ * Checks whether the specified user finshed guide.
+ *
+ * @param user the specified user
+ * @return {@code true} if the specified user finshed guide, returns {@code false} otherwise
+ */
+ public static boolean finshedGuide(final JSONObject user) {
+ return UserExt.USER_GUIDE_STEP_FIN == user.optInt(UserExt.USER_GUIDE_STEP);
+ }
+
+ /**
+ * Gets user link with the specified user.
+ *
+ * @param user the specified user
+ * @return user link
+ */
+ public static String getUserLink(final JSONObject user) {
+ return getUserLink(user.optString(User.USER_NAME));
+ }
+
+ /**
+ * Gets user link with the specified user name.
+ *
+ * @param userName the specified user name
+ * @return user link
+ */
+ public static String getUserLink(final String userName) {
+ return "" + userName + " ";
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Verifycode.java b/src/main/java/org/b3log/symphony/model/Verifycode.java
new file mode 100644
index 000000000..0ac1b3c20
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Verifycode.java
@@ -0,0 +1,112 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all verifycode model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.2.0.0, Jun 12, 2018
+ * @since 1.3.0
+ */
+public final class Verifycode {
+
+ /**
+ * Verifycode.
+ */
+ public static final String VERIFYCODE = "verifycode";
+
+ /**
+ * Verifycodes.
+ */
+ public static final String VERIFYCODES = "verifycodes";
+
+ /**
+ * Key of user id.
+ */
+ public static final String USER_ID = "userId";
+
+ /**
+ * Key of type.
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * Key of business type.
+ */
+ public static final String BIZ_TYPE = "bizType";
+
+ /**
+ * Key of receiver.
+ */
+ public static final String RECEIVER = "receiver";
+
+ /**
+ * Key of code.
+ */
+ public static final String CODE = "code";
+
+ /**
+ * Key of status.
+ */
+ public static final String STATUS = "status";
+
+ /**
+ * Key of expired.
+ */
+ public static final String EXPIRED = "expired";
+
+ // Type constants
+ /**
+ * Type - Email.
+ */
+ public static final int TYPE_C_EMAIL = 0;
+
+ // Business type constants
+ /**
+ * Business type - Register.
+ */
+ public static final int BIZ_TYPE_C_REGISTER = 0;
+
+ /**
+ * Business type - Reset password.
+ */
+ public static final int BIZ_TYPE_C_RESET_PWD = 1;
+
+ /**
+ * Business type - Bind email.
+ */
+ public static final int BIZ_TYPE_C_BIND_EMAIL = 3;
+
+ // Status constants
+ /**
+ * Status - Unsent.
+ */
+ public static final int STATUS_C_UNSENT = 0;
+
+ /**
+ * Status - Sent.
+ */
+ public static final int STATUS_C_SENT = 1;
+
+ /**
+ * Private constructor.
+ */
+ private Verifycode() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/Visit.java b/src/main/java/org/b3log/symphony/model/Visit.java
new file mode 100644
index 000000000..e8260d3b7
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Visit.java
@@ -0,0 +1,84 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all visit model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jul 27, 2018
+ * @since 3.2.0
+ */
+public final class Visit {
+
+ /**
+ * Visit.
+ */
+ public static final String VISIT = "visit";
+
+ /**
+ * Visits.
+ */
+ public static final String VISITS = "visits";
+
+ /**
+ * Key of visit URL.
+ */
+ public static final String VISIT_URL = "visitURL";
+
+ /**
+ * Key of visit IP.
+ */
+ public static final String VISIT_IP = "visitIP";
+
+ /**
+ * Key of visit UA.
+ */
+ public static final String VISIT_UA = "visitUA";
+
+ /**
+ * Key of visit city.
+ */
+ public static final String VISIT_CITY = "visitCity";
+
+ /**
+ * Key of visit device id.
+ */
+ public static final String VISIT_DEVICE_ID = "visitDeviceId";
+
+ /**
+ * Key of visit user id.
+ */
+ public static final String VISIT_USER_ID = "visitUserId";
+
+ /**
+ * Key of visit referer URL.
+ */
+ public static final String VISIT_REFERER_URL = "visitRefererURL";
+
+ /**
+ * Key of visit created.
+ */
+ public static final String VISIT_CREATED = "visitCreated";
+
+ /**
+ * Key of visit expired.
+ */
+ public static final String VISIT_EXPIRED = "visitExpired";
+
+}
diff --git a/src/main/java/org/b3log/symphony/model/Vote.java b/src/main/java/org/b3log/symphony/model/Vote.java
new file mode 100644
index 000000000..334942d67
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/Vote.java
@@ -0,0 +1,96 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model;
+
+/**
+ * This class defines all vote model relevant keys.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Aug 13, 2015
+ * @since 1.3.0
+ */
+public final class Vote {
+
+ /**
+ * Vote.
+ */
+ public static final String VOTE = "vote";
+
+ /**
+ * Votes.
+ */
+ public static final String VOTES = "votes";
+
+ /**
+ * Key of user id.
+ */
+ public static final String USER_ID = "userId";
+
+ /**
+ * Key of type.
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * Key of data type.
+ */
+ public static final String DATA_TYPE = "dataType";
+
+ /**
+ * Key of data id.
+ */
+ public static final String DATA_ID = "dataId";
+
+ // Type constants
+ /**
+ * Type - Up.
+ */
+ public static final int TYPE_C_UP = 0;
+
+ /**
+ * Type - Down.
+ */
+ public static final int TYPE_C_DOWN = 1;
+
+ // Data Type constants
+ /**
+ * Data Type - Article.
+ */
+ public static final int DATA_TYPE_C_ARTICLE = 0;
+
+ /**
+ * Data Type - Comment.
+ */
+ public static final int DATA_TYPE_C_COMMENT = 1;
+
+ /**
+ * Data Type - User.
+ */
+ public static final int DATA_TYPE_C_USER = 2;
+
+ /**
+ * Data Type - Tag.
+ */
+ public static final int DATA_TYPE_C_TAG = 3;
+
+ /**
+ * Private constructor.
+ */
+ private Vote() {
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/feed/RSSCategory.java b/src/main/java/org/b3log/symphony/model/feed/RSSCategory.java
new file mode 100644
index 000000000..3ce287627
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/feed/RSSCategory.java
@@ -0,0 +1,64 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model.feed;
+
+import org.apache.commons.lang.StringEscapeUtils;
+
+/**
+ * Category.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jul 5, 2018
+ * @since 3.1.0
+ */
+public final class RSSCategory {
+
+ /**
+ * Category element.
+ */
+ private static final String CATEGORY_ELEMENT = "${term} ";
+
+ /**
+ * Term.
+ */
+ private String term;
+
+ /**
+ * Gets the term.
+ *
+ * @return term
+ */
+ public String getTerm() {
+ return term;
+ }
+
+ /**
+ * Sets the term with the specified term.
+ *
+ * @param term the specified term
+ */
+ public void setTerm(final String term) {
+ this.term = term;
+ }
+
+ @Override
+ public String toString() {
+ return CATEGORY_ELEMENT.replace("${term}", StringEscapeUtils.escapeXml(term));
+ }
+}
+
diff --git a/src/main/java/org/b3log/symphony/model/feed/RSSChannel.java b/src/main/java/org/b3log/symphony/model/feed/RSSChannel.java
new file mode 100644
index 000000000..2f93ea1cf
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/feed/RSSChannel.java
@@ -0,0 +1,339 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model.feed;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.b3log.latke.util.XMLs;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * RSS 2.0 channel.
+ *
+ * See RSS 2.0 at Harvard Law for more details.
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jul 5, 2018
+ * @see RSSItem
+ * @see RSSCategory
+ * @since 3.1.0
+ */
+public final class RSSChannel {
+
+ /**
+ * Start.
+ */
+ private static final String START = "";
+
+ /**
+ * End.
+ */
+ private static final String END = " ";
+
+ /**
+ * Start title element.
+ */
+ private static final String START_TITLE_ELEMENT = "";
+
+ /**
+ * End title element.
+ */
+ private static final String END_TITLE_ELEMENT = " ";
+
+ /**
+ * Start link element.
+ */
+ private static final String START_LINK_ELEMENT = " ";
+
+ /**
+ * Atom link variable.
+ */
+ private static final String ATOM_LINK_VARIABLE = "${atomLink}";
+
+ /**
+ * End link element.
+ */
+ private static final String END_LINK_ELEMENT = "";
+
+ /**
+ * Atom link element.
+ */
+ private static final String ATOM_LINK_ELEMENT = " ";
+
+ /**
+ * Start description element.
+ */
+ private static final String START_DESCRIPTION_ELEMENT = "";
+
+ /**
+ * End description element.
+ */
+ private static final String END_DESCRIPTION_ELEMENT = " ";
+
+ /**
+ * Start generator element.
+ */
+ private static final String START_GENERATOR_ELEMENT = "";
+
+ /**
+ * End generator element.
+ */
+ private static final String END_GENERATOR_ELEMENT = " ";
+
+ /**
+ * Start language element.
+ */
+ private static final String START_LANGUAGE_ELEMENT = "";
+
+ /**
+ * End language element.
+ */
+ private static final String END_LANGUAGE_ELEMENT = " ";
+
+ /**
+ * Start last build date element.
+ */
+ private static final String START_LAST_BUILD_DATE_ELEMENT = "";
+
+ /**
+ * End last build date element.
+ */
+ private static final String END_LAST_BUILD_DATE_ELEMENT = " ";
+
+ /**
+ * Title.
+ */
+ private String title;
+
+ /**
+ * Link.
+ */
+ private String link;
+
+ /**
+ * Atom link.
+ */
+ private String atomLink;
+
+ /**
+ * Description.
+ */
+ private String description;
+
+ /**
+ * Generator.
+ */
+ private String generator;
+
+ /**
+ * Last build date.
+ */
+ private Date lastBuildDate;
+
+ /**
+ * Language.
+ */
+ private String language;
+
+ /**
+ * Items.
+ */
+ private List items = new ArrayList<>();
+
+ /**
+ * Gets the atom link.
+ *
+ * @return atom link
+ */
+ public String getAtomLink() {
+ return atomLink;
+ }
+
+ /**
+ * Sets the atom link with the specified atom link.
+ *
+ * @param atomLink the specified atom link
+ */
+ public void setAtomLink(final String atomLink) {
+ this.atomLink = atomLink;
+ }
+
+ /**
+ * Gets the last build date.
+ *
+ * @return last build date
+ */
+ public Date getLastBuildDate() {
+ return lastBuildDate;
+ }
+
+ /**
+ * Sets the last build date with the specified last build date.
+ *
+ * @param lastBuildDate the specified last build date
+ */
+ public void setLastBuildDate(final Date lastBuildDate) {
+ this.lastBuildDate = lastBuildDate;
+ }
+
+ /**
+ * Gets generator.
+ *
+ * @return generator
+ */
+ public String getGenerator() {
+ return generator;
+ }
+
+ /**
+ * Sets the generator with the specified generator.
+ *
+ * @param generator the specified generator
+ */
+ public void setGenerator(final String generator) {
+ this.generator = generator;
+ }
+
+ /**
+ * Gets the link.
+ *
+ * @return link
+ */
+ public String getLink() {
+ return link;
+ }
+
+ /**
+ * Sets the link with the specified link.
+ *
+ * @param link the specified link
+ */
+ public void setLink(final String link) {
+ this.link = link;
+ }
+
+ /**
+ * Gets the title.
+ *
+ * @return title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Sets the title with the specified title.
+ *
+ * @param title the specified title
+ */
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ /**
+ * Adds the specified item.
+ *
+ * @param item the specified item
+ */
+ public void addItem(final RSSItem item) {
+ items.add(item);
+ }
+
+ /**
+ * Gets the description.
+ *
+ * @return description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the description with the specified description.
+ *
+ * @param description the specified description
+ */
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ /**
+ * Gets the language.
+ *
+ * @return language
+ */
+ public String getLanguage() {
+ return language;
+ }
+
+ /**
+ * Sets the language with the specified language.
+ *
+ * @param language the specified language
+ */
+ public void setLanguage(final String language) {
+ this.language = language;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append(START);
+
+ stringBuilder.append(START_TITLE_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(title));
+ stringBuilder.append(END_TITLE_ELEMENT);
+
+ stringBuilder.append(START_LINK_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(link));
+ stringBuilder.append(END_LINK_ELEMENT);
+
+ stringBuilder.append(ATOM_LINK_ELEMENT.replace(ATOM_LINK_VARIABLE, atomLink));
+
+ stringBuilder.append(START_DESCRIPTION_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(description));
+ stringBuilder.append(END_DESCRIPTION_ELEMENT);
+
+ stringBuilder.append(START_GENERATOR_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(generator));
+ stringBuilder.append(END_GENERATOR_ELEMENT);
+
+ stringBuilder.append(START_LAST_BUILD_DATE_ELEMENT);
+ stringBuilder.append(DateFormatUtils.SMTP_DATETIME_FORMAT.format(lastBuildDate));
+ stringBuilder.append(END_LAST_BUILD_DATE_ELEMENT);
+
+ stringBuilder.append(START_LANGUAGE_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(language));
+ stringBuilder.append(END_LANGUAGE_ELEMENT);
+
+ for (final RSSItem item : items) {
+ stringBuilder.append(item.toString());
+ }
+
+ stringBuilder.append(END);
+
+ return XMLs.format(stringBuilder.toString());
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/feed/RSSItem.java b/src/main/java/org/b3log/symphony/model/feed/RSSItem.java
new file mode 100644
index 000000000..90565aa72
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/feed/RSSItem.java
@@ -0,0 +1,283 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model.feed;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Item.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Aug 20, 2018
+ * @since 3.1.0
+ */
+public final class RSSItem {
+
+ /**
+ * Start title element.
+ */
+ private static final String START_TITLE_ELEMENT = "";
+
+ /**
+ * End title element.
+ */
+ private static final String END_TITLE_ELEMENT = " ";
+
+ /**
+ * Start link element.
+ */
+ private static final String START_LINK_ELEMENT = " ";
+
+ /**
+ * End link element.
+ */
+ private static final String END_LINK_ELEMENT = "";
+
+ /**
+ * Start description element.
+ */
+ private static final String START_DESCRIPTION_ELEMENT = "";
+
+ /**
+ * End summary element.
+ */
+ private static final String END_DESCRIPTION_ELEMENT = " ";
+
+ /**
+ * Start author element.
+ */
+ private static final String START_AUTHOR_ELEMENT = "";
+
+ /**
+ * End author element.
+ */
+ private static final String END_AUTHOR_ELEMENT = " ";
+
+ /**
+ * Categories.
+ */
+ private Set categories = new HashSet<>();
+
+ /**
+ * Start guid element.
+ */
+ private static final String START_GUID_ELEMENT = "";
+
+ /**
+ * End guid element.
+ */
+ private static final String END_GUID_ELEMENT = " ";
+
+ /**
+ * Start pubDate element.
+ */
+ private static final String START_PUB_DATE_ELEMENT = "";
+
+ /**
+ * End pubDate element.
+ */
+ private static final String END_PUB_DATE_ELEMENT = " ";
+
+ /**
+ * Guid.
+ */
+ private String guid;
+
+ /**
+ * Publish date.
+ */
+ private Date pubDate;
+
+ /**
+ * Title.
+ */
+ private String title;
+
+ /**
+ * Description.
+ */
+ private String description;
+
+ /**
+ * Link.
+ */
+ private String link;
+
+ /**
+ * Author.
+ */
+ private String author;
+
+ /**
+ * Gets the GUID.
+ *
+ * @return GUID
+ */
+ public String getGUID() {
+ return guid;
+ }
+
+ /**
+ * Sets the GUID with the specified GUID.
+ *
+ * @param guid the specified GUID
+ */
+ public void setGUID(final String guid) {
+ this.guid = guid;
+ }
+
+ /**
+ * Gets the author.
+ *
+ * @return author
+ */
+ public String getAuthor() {
+ return author;
+ }
+
+ /**
+ * Sets the author with the specified author.
+ *
+ * @param author the specified author
+ */
+ public void setAuthor(final String author) {
+ this.author = author;
+ }
+
+ /**
+ * Gets the link.
+ *
+ * @return link
+ */
+ public String getLink() {
+ return link;
+ }
+
+ /**
+ * Sets the link with the specified link.
+ *
+ * @param link the specified link
+ */
+ public void setLink(final String link) {
+ this.link = link;
+ }
+
+ /**
+ * Gets the title.
+ *
+ * @return title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Sets the title with the specified title.
+ *
+ * @param title the specified title
+ */
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ /**
+ * Gets publish date.
+ *
+ * @return publish date
+ */
+ public Date getPubDate() {
+ return pubDate;
+ }
+
+ /**
+ * Sets the publish date with the specified publish date.
+ *
+ * @param pubDate the specified publish date
+ */
+ public void setPubDate(final Date pubDate) {
+ this.pubDate = pubDate;
+ }
+
+ /**
+ * Gets the description.
+ *
+ * @return description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the description with the specified description.
+ *
+ * @param description the specified description
+ */
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ /**
+ * Adds the specified category.
+ *
+ * @param category the specified category
+ */
+ public void addCatetory(final RSSCategory category) {
+ categories.add(category);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append("- ").append(START_TITLE_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(title));
+ stringBuilder.append(END_TITLE_ELEMENT);
+
+ stringBuilder.append(START_LINK_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(link));
+ stringBuilder.append(END_LINK_ELEMENT);
+
+ stringBuilder.append(START_DESCRIPTION_ELEMENT);
+ stringBuilder.append("");
+ stringBuilder.append(END_DESCRIPTION_ELEMENT);
+
+ stringBuilder.append(START_AUTHOR_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(author));
+ stringBuilder.append(END_AUTHOR_ELEMENT);
+
+ stringBuilder.append(START_GUID_ELEMENT);
+ stringBuilder.append(StringEscapeUtils.escapeXml(guid));
+ stringBuilder.append(END_GUID_ELEMENT);
+
+ for (final RSSCategory category : categories) {
+ stringBuilder.append(category.toString());
+ }
+
+ stringBuilder.append(START_PUB_DATE_ELEMENT);
+ stringBuilder.append(DateFormatUtils.format(pubDate, "EEE, dd MMM yyyy HH:mm:ss Z", Locale.US));
+ stringBuilder.append(END_PUB_DATE_ELEMENT).append("
");
+
+ return stringBuilder.toString();
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/model/sitemap/Sitemap.java b/src/main/java/org/b3log/symphony/model/sitemap/Sitemap.java
new file mode 100644
index 000000000..5d88af6ed
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/model/sitemap/Sitemap.java
@@ -0,0 +1,268 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.model.sitemap;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sitemap.
+ *
+ *
+ * See Sitemap XML format
+ * for more details.
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.1.0, Nov 15, 2016
+ * @since 1.6.0
+ */
+public final class Sitemap {
+
+ /**
+ * Start document.
+ */
+ private static final String START_DOCUMENT = "";
+
+ /**
+ * Start URL set element.
+ */
+ private static final String START_URL_SET_ELEMENT = "";
+
+ /**
+ * End URL set element.
+ */
+ private static final String END_URL_SET_ELEMENT = " ";
+
+ /**
+ * URLs.
+ */
+ private final List urls = new ArrayList<>();
+
+ /**
+ * Adds the specified url.
+ *
+ * @param url the specified url
+ */
+ public void addURL(final URL url) {
+ urls.add(url);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append(START_DOCUMENT);
+ stringBuilder.append(START_URL_SET_ELEMENT);
+
+ for (final URL url : urls) {
+ stringBuilder.append(url.toString());
+ }
+
+ stringBuilder.append(END_URL_SET_ELEMENT);
+
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Sitemap URL.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Sep 24, 2016
+ * @since 1.6.0
+ */
+ public static final class URL {
+
+ /**
+ * Start URL element.
+ */
+ private static final String START_URL_ELEMENT = "";
+
+ /**
+ * End URL element.
+ */
+ private static final String END_URL_ELEMENT = " ";
+
+ /**
+ * Start location element.
+ */
+ private static final String START_LOC_ELEMENT = "";
+
+ /**
+ * End location element.
+ */
+ private static final String END_LOC_ELEMENT = " ";
+
+ /**
+ * Start last modified element.
+ */
+ private static final String START_LAST_MOD_ELEMENT = "";
+
+ /**
+ * End last modified element.
+ */
+ private static final String END_LAST_MOD_ELEMENT = " ";
+
+ /**
+ * Start change frequency element.
+ */
+ private static final String START_CHANGE_REQ_ELEMENT = "";
+
+ /**
+ * End change frequency element.
+ */
+ private static final String END_CHANGE_REQ_ELEMENT = " ";
+
+ /**
+ * Start priority element.
+ */
+ private static final String START_PRIORITY_ELEMENT = "";
+
+ /**
+ * End priority element.
+ */
+ private static final String END_PRIORITY_ELEMENT = " ";
+
+ /**
+ * Location.
+ */
+ private String loc;
+
+ /**
+ * Last modified.
+ */
+ private String lastMod;
+
+ /**
+ * Change frequency.
+ */
+ private String changeFreq;
+
+ /**
+ * Priority.
+ */
+ private String priority;
+
+ /**
+ * Gets the last modified.
+ *
+ * @return last modified
+ */
+ public String getLastMod() {
+ return lastMod;
+ }
+
+ /**
+ * Sets the last modified with the specified last modified.
+ *
+ * @param lastMod the specified modified
+ */
+ public void setLastMod(final String lastMod) {
+ this.lastMod = lastMod;
+ }
+
+ /**
+ * Gets the location.
+ *
+ * @return location
+ */
+ public String getLoc() {
+ return loc;
+ }
+
+ /**
+ * Sets the location with the specified location.
+ *
+ * @param loc the specified location
+ */
+ public void setLoc(final String loc) {
+ this.loc = loc;
+ }
+
+ /**
+ * Gets the change frequency.
+ *
+ * @return change frequency
+ */
+ public String getChangeFreq() {
+ return changeFreq;
+ }
+
+ /**
+ * Sets the change frequency with the specified change frequency.
+ *
+ * @param changeFreq the specified change frequency
+ */
+ public void setChangeFreq(final String changeFreq) {
+ this.changeFreq = changeFreq;
+ }
+
+ /**
+ * Gets the priority.
+ *
+ * @return priority
+ */
+ public String getPriority() {
+ return priority;
+ }
+
+ /**
+ * Sets the priority with the specified priority.
+ *
+ * @param priority the specified priority
+ */
+ public void setPriority(final String priority) {
+ this.priority = priority;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder stringBuilder = new StringBuilder();
+
+ stringBuilder.append(START_URL_ELEMENT);
+
+ stringBuilder.append(START_LOC_ELEMENT);
+ stringBuilder.append(loc);
+ stringBuilder.append(END_LOC_ELEMENT);
+
+ if (StringUtils.isNotBlank(lastMod)) {
+ stringBuilder.append(START_LAST_MOD_ELEMENT);
+ stringBuilder.append(lastMod);
+ stringBuilder.append(END_LAST_MOD_ELEMENT);
+ }
+
+ if (StringUtils.isNotBlank(changeFreq)) {
+ stringBuilder.append(START_CHANGE_REQ_ELEMENT);
+ stringBuilder.append(changeFreq);
+ stringBuilder.append(END_CHANGE_REQ_ELEMENT);
+ }
+
+ if (StringUtils.isNotBlank(priority)) {
+ stringBuilder.append(START_PRIORITY_ELEMENT);
+ stringBuilder.append(priority);
+ stringBuilder.append(END_PRIORITY_ELEMENT);
+ }
+
+ stringBuilder.append(END_URL_ELEMENT);
+
+ return stringBuilder.toString();
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ActivityProcessor.java b/src/main/java/org/b3log/symphony/processor/ActivityProcessor.java
new file mode 100644
index 000000000..9978e0ef1
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ActivityProcessor.java
@@ -0,0 +1,531 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Pointtransfer;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.CSRFCheck;
+import org.b3log.symphony.processor.advice.CSRFToken;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.Activity1A0001CollectValidation;
+import org.b3log.symphony.processor.advice.validate.Activity1A0001Validation;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Activity processor.
+ *
+ * Shows activities (/activities), GET
+ * Daily checkin (/activity/daily-checkin), GET
+ * Shows 1A0001 (/activity/1A0001), GET
+ * Bets 1A0001 (/activity/1A0001/bet), POST
+ * Collects 1A0001 (/activity/1A0001/collect), POST
+ * Shows character (/activity/character), GET
+ * Submit character (/activity/character/submit), POST
+ * Shows eating snake (/activity/eating-snake), GET
+ * Starts eating snake (/activity/eating-snake/start), POST
+ * Collects eating snake(/activity/eating-snake/collect), POST
+ * Shows gobang (/activity/gobang), GET
+ * Starts gobang (/activity/gobang/start), POST
+ *
+ *
+ * @author Liang Ding
+ * @author Zephyr
+ * @version 1.9.1.15, Jan 21, 2019
+ * @since 1.3.0
+ */
+@RequestProcessor
+public class ActivityProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ActivityProcessor.class);
+
+ /**
+ * Activity management service.
+ */
+ @Inject
+ private ActivityMgmtService activityMgmtService;
+
+ /**
+ * Activity query service.
+ */
+ @Inject
+ private ActivityQueryService activityQueryService;
+
+ /**
+ * Character query service.
+ */
+ @Inject
+ private CharacterQueryService characterQueryService;
+
+ /**
+ * Pointtransfer query service.
+ */
+ @Inject
+ private PointtransferQueryService pointtransferQueryService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Shows 1A0001.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/character", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showCharacter(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "activity/character.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ final JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+
+ String activityCharacterGuideLabel = langPropsService.get("activityCharacterGuideLabel");
+
+ final String character = characterQueryService.getUnwrittenCharacter(userId);
+ if (StringUtils.isBlank(character)) {
+ dataModel.put("noCharacter", true);
+
+ return;
+ }
+
+ final int totalCharacterCount = characterQueryService.getTotalCharacterCount();
+ final int writtenCharacterCount = characterQueryService.getWrittenCharacterCount();
+ final String totalProgress = String.format("%.2f", (double) writtenCharacterCount / (double) totalCharacterCount * 100);
+ dataModel.put("totalProgress", totalProgress);
+
+ final int userWrittenCharacterCount = characterQueryService.getWrittenCharacterCount(userId);
+ final String userProgress = String.format("%.2f", (double) userWrittenCharacterCount / (double) totalCharacterCount * 100);
+ dataModel.put("userProgress", userProgress);
+
+ activityCharacterGuideLabel = activityCharacterGuideLabel.replace("{character}", character);
+ dataModel.put("activityCharacterGuideLabel", activityCharacterGuideLabel);
+ }
+
+ /**
+ * Submits character.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/character/submit", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void submitCharacter(final RequestContext context) {
+ final Request request = context.getRequest();
+ context.renderJSON().renderFalseResult();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Submits character failed", e);
+
+ context.renderJSON(false).renderMsg(langPropsService.get("activityCharacterRecognizeFailedLabel"));
+
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final String dataURL = requestJSONObject.optString("dataURL");
+ final String dataPart = StringUtils.substringAfter(dataURL, ",");
+ final String character = requestJSONObject.optString("character");
+
+ final JSONObject result = activityMgmtService.submitCharacter(userId, dataPart, character);
+ context.renderJSON(result);
+ }
+
+ /**
+ * Shows activity page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activities", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showActivities(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/activities.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put("pointActivityCheckinMin", Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKIN_MIN);
+ dataModel.put("pointActivityCheckinMax", Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKIN_MAX);
+ dataModel.put("pointActivityCheckinStreak", Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKINT_STREAK);
+ dataModel.put("activitYesterdayLivenessRewardMaxPoint", Symphonys.ACTIVITY_YESTERDAY_REWARD_MAX);
+ }
+
+ /**
+ * Shows daily checkin page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/checkin", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showDailyCheckin(final RequestContext context) {
+ final JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ if (activityQueryService.isCheckedinToday(userId)) {
+ context.sendRedirect(Latkes.getServePath() + "/member/" + user.optString(User.USER_NAME) + "/points");
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "activity/checkin.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Daily checkin.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/daily-checkin", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void dailyCheckin(final RequestContext context) {
+ final JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ activityMgmtService.dailyCheckin(userId);
+
+ context.sendRedirect(Latkes.getServePath() + "/member/" + user.optString(User.USER_NAME) + "/points");
+ }
+
+ /**
+ * Yesterday liveness reward.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/yesterday-liveness-reward", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void yesterdayLivenessReward(final RequestContext context) {
+ final Request request = context.getRequest();
+ final JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+
+ activityMgmtService.yesterdayLivenessReward(userId);
+
+ context.sendRedirect(Latkes.getServePath() + "/member/" + user.optString(User.USER_NAME) + "/points");
+ }
+
+ /**
+ * Shows 1A0001.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/1A0001", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void show1A0001(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "activity/1A0001.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean closed = Symphonys.ACTIVITY_1A0001_CLOSED;
+ dataModel.put(Common.CLOSED, closed);
+
+ final Calendar calendar = Calendar.getInstance();
+ final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
+ final boolean closed1A0001 = dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;
+ dataModel.put(Common.CLOSED_1A0001, closed1A0001);
+
+ final int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ final int minute = calendar.get(Calendar.MINUTE);
+ final boolean end = hour > 14 || (hour == 14 && minute > 55);
+ dataModel.put(Common.END, end);
+
+ final boolean collected = activityQueryService.isCollected1A0001Today(userId);
+ dataModel.put(Common.COLLECTED, collected);
+
+ final boolean participated = activityQueryService.is1A0001Today(userId);
+ dataModel.put(Common.PARTICIPATED, participated);
+
+ while (true) {
+ if (closed) {
+ dataModel.put(Keys.MSG, langPropsService.get("activityClosedLabel"));
+ break;
+ }
+
+ if (closed1A0001) {
+ dataModel.put(Keys.MSG, langPropsService.get("activity1A0001CloseLabel"));
+ break;
+ }
+
+ if (collected) {
+ dataModel.put(Keys.MSG, langPropsService.get("activityParticipatedLabel"));
+ break;
+ }
+
+ if (participated) {
+ dataModel.put(Common.HOUR, hour);
+
+ final List records = pointtransferQueryService.getLatestPointtransfers(userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_1A0001, 1);
+ final JSONObject pointtransfer = records.get(0);
+ final String data = pointtransfer.optString(Pointtransfer.DATA_ID);
+ final String smallOrLarge = data.split("-")[1];
+ final int sum = pointtransfer.optInt(Pointtransfer.SUM);
+ String msg = langPropsService.get("activity1A0001BetedLabel");
+ final String small = langPropsService.get("activity1A0001BetSmallLabel");
+ final String large = langPropsService.get("activity1A0001BetLargeLabel");
+ msg = msg.replace("{smallOrLarge}", StringUtils.equals(smallOrLarge, "0") ? small : large);
+ msg = msg.replace("{point}", String.valueOf(sum));
+
+ dataModel.put(Keys.MSG, msg);
+
+ break;
+ }
+
+ if (end) {
+ dataModel.put(Keys.MSG, langPropsService.get("activityEndLabel"));
+ break;
+ }
+
+ break;
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Bets 1A0001.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/1A0001/bet", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, Activity1A0001Validation.class})
+ @After(StopwatchEndAdvice.class)
+ public void bet1A0001(final RequestContext context) {
+ context.renderJSON().renderFalseResult();
+
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+
+ final int amount = requestJSONObject.optInt(Common.AMOUNT);
+ final int smallOrLarge = requestJSONObject.optInt(Common.SMALL_OR_LARGE);
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String fromId = currentUser.optString(Keys.OBJECT_ID);
+
+ final JSONObject ret = activityMgmtService.bet1A0001(fromId, amount, smallOrLarge);
+ if (ret.optBoolean(Keys.STATUS_CODE)) {
+ String msg = langPropsService.get("activity1A0001BetedLabel");
+ final String small = langPropsService.get("activity1A0001BetSmallLabel");
+ final String large = langPropsService.get("activity1A0001BetLargeLabel");
+ msg = msg.replace("{smallOrLarge}", smallOrLarge == 0 ? small : large);
+ msg = msg.replace("{point}", String.valueOf(amount));
+
+ context.renderTrueResult().renderMsg(msg);
+ }
+ }
+
+ /**
+ * Collects 1A0001.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/1A0001/collect", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, Activity1A0001CollectValidation.class})
+ @After(StopwatchEndAdvice.class)
+ public void collect1A0001(final RequestContext context) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ final JSONObject ret = activityMgmtService.collect1A0001(userId);
+
+ context.renderJSON(ret);
+ }
+
+ /**
+ * Shows eating snake.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/eating-snake", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showEatingSnake(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "activity/eating-snake.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ final List maxUsers = activityQueryService.getTopEatingSnakeUsersMax(10);
+ dataModel.put("maxUsers", maxUsers);
+
+ final List sumUsers = activityQueryService.getTopEatingSnakeUsersSum(10);
+ dataModel.put("sumUsers", sumUsers);
+
+ final JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ final int startPoint = activityQueryService.getEatingSnakeAvgPoint(userId);
+
+ String pointActivityEatingSnake = langPropsService.get("activityStartEatingSnakeTipLabel");
+ pointActivityEatingSnake = pointActivityEatingSnake.replace("{point}", String.valueOf(startPoint));
+ dataModel.put("activityStartEatingSnakeTipLabel", pointActivityEatingSnake);
+ }
+
+ /**
+ * Starts eating snake.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/eating-snake/start", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void startEatingSnake(final RequestContext context) {
+ final Request request = context.getRequest();
+ final JSONObject currentUser = Sessions.getUser();
+ final String fromId = currentUser.optString(Keys.OBJECT_ID);
+
+ final JSONObject ret = activityMgmtService.startEatingSnake(fromId);
+
+ context.renderJSON(ret);
+ }
+
+ /**
+ * Collects eating snake.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/eating-snake/collect", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, StopwatchEndAdvice.class})
+ public void collectEatingSnake(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "activity/eating-snake.ftl");
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ final int score = requestJSONObject.optInt("score");
+ final JSONObject user = Sessions.getUser();
+ final JSONObject ret = activityMgmtService.collectEatingSnake(user.optString(Keys.OBJECT_ID), score);
+
+ context.renderJSON(ret);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Collects eating snake game failed", e);
+
+ context.renderJSON(false).renderMsg("err....");
+ }
+ }
+
+ /**
+ * Shows gobang.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/gobang", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showGobang(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "activity/gobang.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+
+ String pointActivityGobang = langPropsService.get("activityStartGobangTipLabel");
+ pointActivityGobang = pointActivityGobang.replace("{point}", String.valueOf(Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START));
+ dataModel.put("activityStartGobangTipLabel", pointActivityGobang);
+ }
+
+ /**
+ * Starts gobang.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/activity/gobang/start", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void startGobang(final RequestContext context) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+ final JSONObject currentUser = Sessions.getUser();
+
+ final boolean succ = currentUser.optInt(UserExt.USER_POINT) - Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START >= 0;
+ ret.put(Keys.STATUS_CODE, succ);
+ final String msg = succ ? "started" : langPropsService.get("activityStartGobangFailLabel");
+ ret.put(Keys.MSG, msg);
+
+ context.renderJSON(ret);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/AdminProcessor.java b/src/main/java/org/b3log/symphony/processor/AdminProcessor.java
new file mode 100644
index 000000000..254eef936
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/AdminProcessor.java
@@ -0,0 +1,2654 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.math.RandomUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.Response;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.latke.util.CollectionUtils;
+import org.b3log.latke.util.Paginator;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.event.ArticleBaiduSender;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.PermissionCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.UserRegister2Validation;
+import org.b3log.symphony.processor.advice.validate.UserRegisterValidation;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Escapes;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.text.ParseException;
+import java.util.*;
+
+/**
+ * Admin processor.
+ *
+ * Shows admin index (/admin), GET
+ * Shows users (/admin/users), GET
+ * Shows a user (/admin/user/{userId}), GET
+ * Shows add user (/admin/add-user), GET
+ * Adds a user (/admin/add-user), POST
+ * Updates a user (/admin/user/{userId}), POST
+ * Updates a user's email (/admin/user/{userId}/email), POST
+ * Updates a user's username (/admin/user/{userId}/username), POST
+ * Charges a user's point (/admin/user/{userId}/charge-point), POST
+ * Exchanges a user's point (/admin/user/{userId}/exchange-point), POST
+ * Deducts a user's abuse point (/admin/user/{userId}/abuse-point), POST
+ * Shows articles (/admin/articles), GET
+ * Shows an article (/admin/article/{articleId}), GET
+ * Updates an article (/admin/article/{articleId}), POST
+ * Removes an article (/admin/remove-article), POST
+ * Shows add article (/admin/add-article), GET
+ * Adds an article (/admin/add-article), POST
+ * Show comments (/admin/comments), GET
+ * Shows a comment (/admin/comment/{commentId}), GET
+ * Updates a comment (/admin/comment/{commentId}), POST
+ * Removes a comment (/admin/remove-comment), POST
+ * Show breezemoons (/admin/breezemoons), GET
+ * Shows a breezemoon (/admin/breezemoon/{breezemoonId}), GET
+ * Updates a breezemoon (/admin/breezemoon/{breezemoonId}), POST
+ * Removes a breezemoon (/admin/remove-breezemoon), POST
+ * Shows domains (/admin/domains, GET
+ * Show a domain (/admin/domain/{domainId}, GET
+ * Updates a domain (/admin/domain/{domainId}), POST
+ * Shows tags (/admin/tags), GET
+ * Removes unused tags (/admin/tags/remove-unused), POST
+ * Show a tag (/admin/tag/{tagId}), GET
+ * Shows add tag (/admin/add-tag), GET
+ * Adds a tag (/admin/add-tag), POST
+ * Updates a tag (/admin/tag/{tagId}), POST
+ * Generates invitecodes (/admin/invitecodes/generate), POST
+ * Shows invitecodes (/admin/invitecodes), GET
+ * Show an invitecode (/admin/invitecode/{invitecodeId}), GET
+ * Updates an invitecode (/admin/invitecode/{invitecodeId}), POST
+ * Shows miscellaneous (/admin/misc), GET
+ * Updates miscellaneous (/admin/misc), POST
+ * Rebuilds article search index (/admin/search/index), POST
+ * Rebuilds one article search index(/admin/search-index-article), POST
+ * Shows ad (/admin/ad), GET
+ * Updates ad (/admin/ad), POST
+ * Shows role permissions (/admin/role/{roleId}/permissions), GET
+ * Updates role permissions (/admin/role/{roleId}/permissions), POST
+ * Removes an role (/admin/role/{roleId}/remove), POST
+ * Adds an role (/admin/role), POST
+ * Show reports (/admin/reports), GET
+ * Makes a report as handled (/admin/report/{reportId}), GET
+ * Shows audit log (/admin/auditlog), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Bill Ho
+ * @author Liyuan Li
+ * @author qiankunpingtai
+ * @version 2.30.1.4, May 20, 2019
+ * @since 1.1.0
+ */
+@RequestProcessor
+public class AdminProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(AdminProcessor.class);
+
+ /**
+ * Pagination window size.
+ */
+ private static final int WINDOW_SIZE = 15;
+
+ /**
+ * Pagination page size.
+ */
+ private static final int PAGE_SIZE = 60;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Article management service.
+ */
+ @Inject
+ private ArticleMgmtService articleMgmtService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Comment management service.
+ */
+ @Inject
+ private CommentMgmtService commentMgmtService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Option management service.
+ */
+ @Inject
+ private OptionMgmtService optionMgmtService;
+
+ /**
+ * Domain query service.
+ */
+ @Inject
+ private DomainQueryService domainQueryService;
+
+ /**
+ * Tag query service.
+ */
+ @Inject
+ private TagQueryService tagQueryService;
+
+ /**
+ * Domain management service.
+ */
+ @Inject
+ private DomainMgmtService domainMgmtService;
+
+ /**
+ * Tag management service.
+ */
+ @Inject
+ private TagMgmtService tagMgmtService;
+
+ /**
+ * Pointtransfer management service.
+ */
+ @Inject
+ private PointtransferMgmtService pointtransferMgmtService;
+
+ /**
+ * Pointtransfer query service.
+ */
+ @Inject
+ private PointtransferQueryService pointtransferQueryService;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Search management service.
+ */
+ @Inject
+ private SearchMgmtService searchMgmtService;
+
+ /**
+ * Invitecode query service.
+ */
+ @Inject
+ private InvitecodeQueryService invitecodeQueryService;
+
+ /**
+ * Invitecode management service.
+ */
+ @Inject
+ private InvitecodeMgmtService invitecodeMgmtService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ /**
+ * Role management service.
+ */
+ @Inject
+ private RoleMgmtService roleMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Breezemoon query service.
+ */
+ @Inject
+ private BreezemoonQueryService breezemoonQueryService;
+
+ /**
+ * Breezemoon management service.
+ */
+ @Inject
+ private BreezemoonMgmtService breezemoonMgmtService;
+
+ /**
+ * Report management service.
+ */
+ @Inject
+ private ReportMgmtService reportMgmtService;
+
+ /**
+ * Report query service.
+ */
+ @Inject
+ private ReportQueryService reportQueryService;
+
+ /**
+ * Operation management service.
+ */
+ @Inject
+ private OperationMgmtService operationMgmtService;
+
+ /**
+ * Operation query service.
+ */
+ @Inject
+ private OperationQueryService operationQueryService;
+
+ /**
+ * Shows audit log.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/auditlog", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAuditlog(final RequestContext context) {
+ final Request request = context.getRequest();
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/auditlog.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final JSONObject result = operationQueryService.getAuditlogs(requestJSONObject);
+ dataModel.put(Operation.OPERATIONS, CollectionUtils.jsonArrayToList(result.optJSONArray(Operation.OPERATIONS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Makes a report as ignored .
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/report/ignore/{reportId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void makeReportIgnored(final RequestContext context) {
+ final String reportId = context.pathVar("reportId");
+ final Request request = context.getRequest();
+ reportMgmtService.makeReportIgnored(reportId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_MAKE_REPORT_IGNORED, reportId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/reports");
+ }
+
+ /**
+ * Makes a report as handled .
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/report/{reportId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void makeReportHandled(final RequestContext context) {
+ final String reportId = context.pathVar("reportId");
+ final Request request = context.getRequest();
+ reportMgmtService.makeReportHandled(reportId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_MAKE_REPORT_HANDLED, reportId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/reports");
+ }
+
+ /**
+ * Shows reports.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/reports", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showReports(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/reports.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final JSONObject result = reportQueryService.getReports(requestJSONObject);
+ dataModel.put(Report.REPORTS, CollectionUtils.jsonArrayToList(result.optJSONArray(Report.REPORTS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Removes an role.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/role/{roleId}/remove", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void removeRole(final RequestContext context) {
+ final String roleId = context.pathVar("roleId");
+ final Request request = context.getRequest();
+
+ final int count = roleQueryService.countUser(roleId);
+ if (0 < count) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, "Still [" + count + "] users are using this role.");
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final JSONObject role = roleQueryService.getRole(roleId);
+ final String roleName = role.optString(Role.ROLE_NAME);
+ roleMgmtService.removeRole(roleId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_ROLE, roleName));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/roles");
+ }
+
+ /**
+ * Show admin breezemoons.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/breezemoons", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showBreezemoons(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/breezemoons.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final List fields = new ArrayList<>();
+ fields.add(Keys.OBJECT_ID);
+ fields.add(Breezemoon.BREEZEMOON_CONTENT);
+ fields.add(Breezemoon.BREEZEMOON_CREATED);
+ fields.add(Breezemoon.BREEZEMOON_AUTHOR_ID);
+ fields.add(Breezemoon.BREEZEMOON_STATUS);
+ final JSONObject result = breezemoonQueryService.getBreezemoons(requestJSONObject, fields);
+ dataModel.put(Breezemoon.BREEZEMOONS, CollectionUtils.jsonArrayToList(result.optJSONArray(Breezemoon.BREEZEMOONS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows a breezemoon.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/breezemoon/{breezemoonId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showBreezemoon(final RequestContext context) {
+ final String breezemoonId = context.pathVar("breezemoonId");
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/breezemoon.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject breezemoon = breezemoonQueryService.getBreezemoon(breezemoonId);
+ Escapes.escapeHTML(breezemoon);
+ dataModel.put(Breezemoon.BREEZEMOON, breezemoon);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates a breezemoon.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/breezemoon/{breezemoonId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateBreezemoon(final RequestContext context) {
+ final String breezemoonId = context.pathVar("breezemoonId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/breezemoon.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ JSONObject breezemoon = breezemoonQueryService.getBreezemoon(breezemoonId);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ if (name.equals(Breezemoon.BREEZEMOON_STATUS)) {
+ breezemoon.put(name, Integer.valueOf(value));
+ } else {
+ breezemoon.put(name, value);
+ }
+ }
+
+ try {
+ breezemoonMgmtService.updateBreezemoon(breezemoon);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_BREEZEMOON, breezemoonId));
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Updates a breezemoon failed", e);
+
+ return;
+ }
+
+ breezemoon = breezemoonQueryService.getBreezemoon(breezemoonId);
+ dataModel.put(Breezemoon.BREEZEMOON, breezemoon);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Removes a breezemoon.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/remove-breezemoon", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeBreezemoon(final RequestContext context) {
+ final Request request = context.getRequest();
+ final String id = context.param(Common.ID);
+
+ try {
+ breezemoonMgmtService.removeBreezemoon(id);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_BREEZEMOON, id));
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Removes a breezemoon failed", e);
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/breezemoons");
+ }
+
+ /**
+ * Removes unused tags.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/tags/remove-unused", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeUnusedTags(final RequestContext context) {
+ context.renderJSON(true);
+
+ tagMgmtService.removeUnusedTags();
+ operationMgmtService.addOperation(Operation.newOperation(context.getRequest(), Operation.OPERATION_CODE_C_REMOVE_UNUSED_TAGS, ""));
+ }
+
+ /**
+ * Adds an role.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/role", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void addRole(final RequestContext context) {
+ final Request request = context.getRequest();
+ final String roleName = context.param(Role.ROLE_NAME);
+ if (StringUtils.isBlank(roleName)) {
+ context.sendRedirect(Latkes.getServePath() + "/admin/roles");
+
+ return;
+ }
+
+ final String roleDesc = context.param(Role.ROLE_DESCRIPTION);
+
+ final JSONObject role = new JSONObject();
+ role.put(Role.ROLE_NAME, roleName);
+ role.put(Role.ROLE_DESCRIPTION, roleDesc);
+
+ roleMgmtService.addRole(role);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERAIONT_CODE_C_ADD_ROLE, roleName));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/roles");
+ }
+
+ /**
+ * Updates role permissions.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/role/{roleId}/permissions", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void updateRolePermissions(final RequestContext context) {
+ final String roleId = context.pathVar("roleId");
+ final Request request = context.getRequest();
+
+ final Set permissionIds = request.getParameterNames();
+
+ roleMgmtService.updateRolePermissions(roleId, permissionIds);
+ final JSONObject role = roleQueryService.getRole(roleId);
+ final String roleName = role.optString(Role.ROLE_NAME);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_ROLE_PERMS, roleName));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/role/" + roleId + "/permissions");
+ }
+
+ /**
+ * Shows role permissions.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/role/{roleId}/permissions", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showRolePermissions(final RequestContext context) {
+ final String roleId = context.pathVar("roleId");
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/role-permissions.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject role = roleQueryService.getRole(roleId);
+ dataModel.put(Role.ROLE, role);
+
+ final Map> categories = new TreeMap<>();
+
+ final List permissions = roleQueryService.getPermissionsGrant(roleId);
+ for (final JSONObject permission : permissions) {
+ final String label = permission.optString(Keys.OBJECT_ID) + "PermissionLabel";
+ permission.put(Permission.PERMISSION_T_LABEL, langPropsService.get(label));
+
+ String category = permission.optString(Permission.PERMISSION_CATEGORY);
+ category = langPropsService.get(category + "PermissionLabel");
+
+ final List categoryPermissions = categories.computeIfAbsent(category, k -> new ArrayList<>());
+ categoryPermissions.add(permission);
+ }
+
+ dataModel.put(Permission.PERMISSION_T_CATEGORIES, categories);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows roles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/roles", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showRoles(final RequestContext context) {
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/roles.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject result = roleQueryService.getRoles(1, Integer.MAX_VALUE, 10);
+ final List roles = (List) result.opt(Role.ROLES);
+
+ dataModel.put(Role.ROLES, roles);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates side ad.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/ad/side", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateSideAd(final RequestContext context) {
+ final Request request = context.getRequest();
+ final String sideFullAd = context.param("sideFullAd");
+
+ JSONObject adOption = optionQueryService.getOption(Option.ID_C_SIDE_FULL_AD);
+ if (null == adOption) {
+ adOption = new JSONObject();
+ adOption.put(Keys.OBJECT_ID, Option.ID_C_SIDE_FULL_AD);
+ adOption.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_AD);
+ adOption.put(Option.OPTION_VALUE, sideFullAd);
+ optionMgmtService.addOption(adOption);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_AD_POS, Option.ID_C_SIDE_FULL_AD));
+ } else {
+ adOption.put(Option.OPTION_VALUE, sideFullAd);
+ optionMgmtService.updateOption(Option.ID_C_SIDE_FULL_AD, adOption);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_AD_POS, Option.ID_C_SIDE_FULL_AD));
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/ad");
+ }
+
+ /**
+ * Updates banner.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/ad/banner", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateBanner(final RequestContext context) {
+ final Request request = context.getRequest();
+ final String headerBanner = context.param("headerBanner");
+
+ JSONObject adOption = optionQueryService.getOption(Option.ID_C_HEADER_BANNER);
+ if (null == adOption) {
+ adOption = new JSONObject();
+ adOption.put(Keys.OBJECT_ID, Option.ID_C_HEADER_BANNER);
+ adOption.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_AD);
+ adOption.put(Option.OPTION_VALUE, headerBanner);
+ optionMgmtService.addOption(adOption);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_AD_POS, Option.ID_C_HEADER_BANNER));
+ } else {
+ adOption.put(Option.OPTION_VALUE, headerBanner);
+ optionMgmtService.updateOption(Option.ID_C_HEADER_BANNER, adOption);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_AD_POS, Option.ID_C_HEADER_BANNER));
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/ad");
+ }
+
+ /**
+ * Shows ad.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/ad", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAd(final RequestContext context) {
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/ad.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put("sideFullAd", "");
+ dataModel.put("headerBanner", "");
+
+ final JSONObject sideAdOption = optionQueryService.getOption(Option.ID_C_SIDE_FULL_AD);
+ if (null != sideAdOption) {
+ dataModel.put("sideFullAd", sideAdOption.optString(Option.OPTION_VALUE));
+ }
+
+ final JSONObject headerBanner = optionQueryService.getOption(Option.ID_C_HEADER_BANNER);
+ if (null != headerBanner) {
+ dataModel.put("headerBanner", headerBanner.optString(Option.OPTION_VALUE));
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows add tag.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-tag", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAddTag(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/add-tag.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Adds a tag.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-tag", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void addTag(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ String title = StringUtils.trim(context.param(Tag.TAG_TITLE));
+ try {
+ if (StringUtils.isBlank(title)) {
+ throw new Exception(langPropsService.get("tagsErrorLabel"));
+ }
+
+ title = Tag.formatTags(title);
+
+ if (!Tag.containsWhiteListTags(title)) {
+ if (!Tag.TAG_TITLE_PATTERN.matcher(title).matches()) {
+ throw new Exception(langPropsService.get("tagsErrorLabel"));
+ }
+
+ if (title.length() > Tag.MAX_TAG_TITLE_LENGTH) {
+ throw new Exception(langPropsService.get("tagsErrorLabel"));
+ }
+ }
+ } catch (final Exception e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final JSONObject admin = Sessions.getUser();
+ final String userId = admin.optString(Keys.OBJECT_ID);
+
+ String tagId;
+ try {
+ tagId = tagMgmtService.addTag(userId, title);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_TAG, title));
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/tag/" + tagId);
+ }
+
+ /**
+ * Sticks an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/stick-article", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void stickArticle(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String articleId = context.param(Article.ARTICLE_T_ID);
+ articleMgmtService.adminStick(articleId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_STICK_ARTICLE, articleId));
+ context.sendRedirect(Latkes.getServePath() + "/admin/articles");
+ }
+
+ /**
+ * Cancels stick an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/cancel-stick-article", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void stickCancelArticle(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String articleId = context.param(Article.ARTICLE_T_ID);
+ articleMgmtService.adminCancelStick(articleId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_CANCEL_STICK_ARTICLE, articleId));
+ context.sendRedirect(Latkes.getServePath() + "/admin/articles");
+ }
+
+ /**
+ * Generates invitecodes.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/invitecodes/generate", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void generateInvitecodes(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String quantityStr = context.param("quantity");
+ int quantity = 20;
+ try {
+ quantity = Integer.valueOf(quantityStr);
+ } catch (final NumberFormatException e) {
+ // ignore
+ }
+
+ String memo = context.param("memo");
+ if (StringUtils.isBlank(memo)) {
+ memo = "注册帖";
+ }
+
+ invitecodeMgmtService.adminGenInvitecodes(quantity, memo);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_GENERATE_INVITECODES, quantity + " " + memo));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/invitecodes");
+ }
+
+ /**
+ * Shows admin invitecodes.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/invitecodes", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showInvitecodes(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/invitecodes.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final JSONObject result = invitecodeQueryService.getInvitecodes(requestJSONObject);
+ dataModel.put(Invitecode.INVITECODES, CollectionUtils.jsonArrayToList(result.optJSONArray(Invitecode.INVITECODES)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows an invitecode.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/invitecode/{invitecodeId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showInvitecode(final RequestContext context) {
+ final String invitecodeId = context.pathVar("invitecodeId");
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/invitecode.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject invitecode = invitecodeQueryService.getInvitecodeById(invitecodeId);
+ dataModel.put(Invitecode.INVITECODE, invitecode);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates an invitecode.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/invitecode/{invitecodeId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateInvitecode(final RequestContext context) {
+ final String invitecodeId = context.pathVar("invitecodeId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/invitecode.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ JSONObject invitecode = invitecodeQueryService.getInvitecodeById(invitecodeId);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ invitecode.put(name, value);
+ }
+
+ try {
+ invitecodeMgmtService.updateInvitecode(invitecodeId, invitecode);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_INVITECODE, invitecodeId));
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Updates an invitecode failed", e);
+
+ return;
+ }
+
+ invitecode = invitecodeQueryService.getInvitecodeById(invitecodeId);
+ dataModel.put(Invitecode.INVITECODE, invitecode);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows add article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-article", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAddArticle(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/add-article.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Adds an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-article", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void addArticle(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String userName = context.param(User.USER_NAME);
+ final JSONObject author = userQueryService.getUserByName(userName);
+ if (null == author) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, langPropsService.get("notFoundUserLabel"));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final String timeStr = context.param(Common.TIME);
+ final String articleTitle = context.param(Article.ARTICLE_TITLE);
+ final String articleTags = context.param(Article.ARTICLE_TAGS);
+ final String articleContent = context.param(Article.ARTICLE_CONTENT);
+ String rewardContent = context.param(Article.ARTICLE_REWARD_CONTENT);
+ final String rewardPoint = context.param(Article.ARTICLE_REWARD_POINT);
+ final int articleShowInList = Article.ARTICLE_SHOW_IN_LIST_C_YES;
+ long time = System.currentTimeMillis();
+
+ try {
+ final Date date = DateUtils.parseDate(timeStr, new String[]{"yyyy-MM-dd'T'HH:mm"});
+
+ time = date.getTime();
+ final int random = RandomUtils.nextInt(9999);
+ time += random;
+ } catch (final ParseException e) {
+ LOGGER.log(Level.ERROR, "Parse time failed, using current time instead");
+ }
+
+ final JSONObject article = new JSONObject();
+ article.put(Article.ARTICLE_TITLE, articleTitle);
+ article.put(Article.ARTICLE_TAGS, articleTags);
+ article.put(Article.ARTICLE_CONTENT, articleContent);
+ article.put(Article.ARTICLE_REWARD_CONTENT, rewardContent);
+ article.put(Article.ARTICLE_REWARD_POINT, Integer.valueOf(rewardPoint));
+ article.put(User.USER_NAME, userName);
+ article.put(Common.TIME, time);
+ article.put(Article.ARTICLE_SHOW_IN_LIST, articleShowInList);
+ try {
+ final String articleId = articleMgmtService.addArticleByAdmin(article);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_ARTICLE, articleId));
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/articles");
+ }
+
+ /**
+ * Adds a reserved word.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-reserved-word", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void addReservedWord(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ String word = context.param(Common.WORD);
+ word = StringUtils.trim(word);
+ if (StringUtils.isBlank(word)) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, langPropsService.get("invalidReservedWordLabel"));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ if (optionQueryService.isReservedWord(word)) {
+ context.sendRedirect(Latkes.getServePath() + "/admin/reserved-words");
+
+ return;
+ }
+
+ try {
+ final JSONObject reservedWord = new JSONObject();
+ reservedWord.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_RESERVED_WORDS);
+ reservedWord.put(Option.OPTION_VALUE, word);
+
+ optionMgmtService.addOption(reservedWord);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_RESERVED_WORD, word));
+ } catch (final Exception e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/reserved-words");
+ }
+
+ /**
+ * Shows add reserved word.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-reserved-word", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAddReservedWord(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/add-reserved-word.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates a reserved word.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/reserved-word/{id}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateReservedWord(final RequestContext context) {
+ final String id = context.pathVar("id");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/reserved-word.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject word = optionQueryService.getOption(id);
+ dataModel.put(Common.WORD, word);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ word.put(name, value);
+ }
+
+ optionMgmtService.updateOption(id, word);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_RESERVED_WORD, word.optString(Option.OPTION_VALUE)));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows reserved words.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/reserved-words", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showReservedWords(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/reserved-words.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Common.WORDS, optionQueryService.getReservedWords());
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows a reserved word.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/reserved-word/{id}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showReservedWord(final RequestContext context) {
+ final String id = context.pathVar("id");
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/reserved-word.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject word = optionQueryService.getOption(id);
+ dataModel.put(Common.WORD, word);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Removes a reserved word.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/remove-reserved-word", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeReservedWord(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String id = context.param("id");
+ final JSONObject option = optionQueryService.getOption(id);
+ final String word = option.optString(Option.OPTION_VALUE);
+ optionMgmtService.removeOption(id);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_RESERVED_WORD, word));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/reserved-words");
+ }
+
+ /**
+ * Removes a comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/remove-comment", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeComment(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String commentId = context.param(Comment.COMMENT_T_ID);
+ commentMgmtService.removeCommentByAdmin(commentId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_COMMENT, commentId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/comments");
+ }
+
+ /**
+ * Removes an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/remove-article", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeArticle(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String articleId = context.param(Article.ARTICLE_T_ID);
+ articleMgmtService.removeArticleByAdmin(articleId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_ARTICLE, articleId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/articles");
+ }
+
+ /**
+ * Shows admin index.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAdminIndex(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/index.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ dataModel.put(Common.ONLINE_VISITOR_CNT, optionQueryService.getOnlineVisitorCount());
+ dataModel.put(Common.ONLINE_MEMBER_CNT, optionQueryService.getOnlineMemberCount());
+
+ final JSONObject statistic = optionQueryService.getStatistic();
+ dataModel.put(Option.CATEGORY_C_STATISTIC, statistic);
+ }
+
+ /**
+ * Shows admin users.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/users", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showUsers(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/users.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+ final String query = context.param(Common.QUERY);
+ if (StringUtils.isNotBlank(query)) {
+ requestJSONObject.put(Common.QUERY, query);
+ }
+
+ final JSONObject result = userQueryService.getUsers(requestJSONObject);
+ dataModel.put(User.USERS, CollectionUtils.jsonArrayToList(result.optJSONArray(User.USERS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows a user.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showUser(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/user.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject user = userQueryService.getUser(userId);
+ Escapes.escapeHTML(user);
+ dataModel.put(User.USER, user);
+
+ final JSONObject result = roleQueryService.getRoles(1, Integer.MAX_VALUE, 10);
+ final List roles = (List) result.opt(Role.ROLES);
+ dataModel.put(Role.ROLES, roles);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows add user.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-user", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAddUser(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/add-user.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Adds a user.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-user", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void addUser(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String userName = context.param(User.USER_NAME);
+ final String email = context.param(User.USER_EMAIL);
+ final String password = context.param(User.USER_PASSWORD);
+ final String appRole = context.param(UserExt.USER_APP_ROLE);
+
+ final boolean nameInvalid = UserRegisterValidation.invalidUserName(userName);
+ final boolean emailInvalid = !Strings.isEmail(email);
+ final boolean passwordInvalid = UserRegister2Validation.invalidUserPassword(password);
+
+ if (nameInvalid || emailInvalid || passwordInvalid) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ if (nameInvalid) {
+ dataModel.put(Keys.MSG, langPropsService.get("invalidUserNameLabel"));
+ } else if (emailInvalid) {
+ dataModel.put(Keys.MSG, langPropsService.get("invalidEmailLabel"));
+ } else if (passwordInvalid) {
+ dataModel.put(Keys.MSG, langPropsService.get("invalidPasswordLabel"));
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ String userId;
+ try {
+ final JSONObject user = new JSONObject();
+ user.put(User.USER_NAME, userName);
+ user.put(User.USER_EMAIL, email);
+ user.put(User.USER_PASSWORD, DigestUtils.md5Hex(password));
+ user.put(UserExt.USER_APP_ROLE, appRole);
+ user.put(UserExt.USER_STATUS, UserExt.USER_STATUS_C_VALID);
+
+ final JSONObject admin = Sessions.getUser();
+ user.put(UserExt.USER_LANGUAGE, admin.optString(UserExt.USER_LANGUAGE));
+
+ userId = userMgmtService.addUser(user);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_USER, userId));
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Updates a user.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateUser(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/user.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject user = userQueryService.getUser(userId);
+ dataModel.put(User.USER, user);
+ final String oldRole = user.optString(User.USER_ROLE);
+
+ final JSONObject result = roleQueryService.getRoles(1, Integer.MAX_VALUE, 10);
+ final List roles = (List) result.opt(Role.ROLES);
+ dataModel.put(Role.ROLES, roles);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ switch (name) {
+ case UserExt.USER_POINT:
+ case UserExt.USER_APP_ROLE:
+ case UserExt.USER_STATUS:
+ case UserExt.USER_COMMENT_VIEW_MODE:
+ case UserExt.USER_AVATAR_VIEW_MODE:
+ case UserExt.USER_LIST_PAGE_SIZE:
+ case UserExt.USER_LIST_VIEW_MODE:
+ case UserExt.USER_NOTIFY_STATUS:
+ case UserExt.USER_SUB_MAIL_STATUS:
+ case UserExt.USER_KEYBOARD_SHORTCUTS_STATUS:
+ case UserExt.USER_REPLY_WATCH_ARTICLE_STATUS:
+ case UserExt.USER_GEO_STATUS:
+ case UserExt.USER_ARTICLE_STATUS:
+ case UserExt.USER_COMMENT_STATUS:
+ case UserExt.USER_FOLLOWING_USER_STATUS:
+ case UserExt.USER_FOLLOWING_TAG_STATUS:
+ case UserExt.USER_FOLLOWING_ARTICLE_STATUS:
+ case UserExt.USER_WATCHING_ARTICLE_STATUS:
+ case UserExt.USER_BREEZEMOON_STATUS:
+ case UserExt.USER_FOLLOWER_STATUS:
+ case UserExt.USER_POINT_STATUS:
+ case UserExt.USER_ONLINE_STATUS:
+ case UserExt.USER_UA_STATUS:
+ case UserExt.USER_JOIN_POINT_RANK:
+ case UserExt.USER_JOIN_USED_POINT_RANK:
+ case UserExt.USER_FORWARD_PAGE_STATUS:
+ user.put(name, Integer.valueOf(value));
+
+ break;
+ case User.USER_PASSWORD:
+ final String oldPwd = user.getString(name);
+ if (!oldPwd.equals(value) && StringUtils.isNotBlank(value)) {
+ user.put(name, DigestUtils.md5Hex(value));
+ }
+
+ break;
+ default:
+ user.put(name, value);
+
+ break;
+ }
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
+ user.put(User.USER_ROLE, oldRole);
+ }
+
+ try {
+ userMgmtService.updateUser(userId, user);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_USER, userId));
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Updates a user failed", e);
+
+ return;
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates a user's email.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}/email", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateUserEmail(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+
+ final JSONObject user = userQueryService.getUser(userId);
+ final String oldEmail = user.optString(User.USER_EMAIL);
+ final String newEmail = context.param(User.USER_EMAIL);
+
+ if (oldEmail.equals(newEmail)) {
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+
+ return;
+ }
+
+ user.put(User.USER_EMAIL, newEmail);
+
+ try {
+ userMgmtService.updateUserEmail(userId, user);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_USER_EMAIL, userId));
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Updates a user's username.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}/username", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateUserName(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+
+ final JSONObject user = userQueryService.getUser(userId);
+ final String oldUserName = user.optString(User.USER_NAME);
+ final String newUserName = context.param(User.USER_NAME);
+
+ if (oldUserName.equals(newUserName)) {
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+
+ return;
+ }
+
+ user.put(User.USER_NAME, newUserName);
+
+ try {
+ userMgmtService.updateUserName(userId, user);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_USER_NAME, userId));
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Charges a user's point.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}/charge-point", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void chargePoint(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+
+ final String pointStr = context.param(Common.POINT);
+ final String memo = context.param("memo");
+
+ if (StringUtils.isBlank(pointStr) || StringUtils.isBlank(memo) || !Strings.isNumeric(memo.split("-")[0])) {
+ LOGGER.warn("Charge point memo format error");
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+
+ return;
+ }
+
+ try {
+ final int point = Integer.valueOf(pointStr);
+
+ final String transferId = pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_CHARGE, point, memo, System.currentTimeMillis(), "");
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_CHARGE_POINT, transferId));
+
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, userId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, transferId);
+ notificationMgmtService.addPointChargeNotification(notification);
+ } catch (final NumberFormatException | ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Deducts a user's abuse point.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}/abuse-point", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void abusePoint(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+
+ final String pointStr = context.param(Common.POINT);
+
+ try {
+ final int point = Integer.valueOf(pointStr);
+
+ final JSONObject user = userQueryService.getUser(userId);
+ final int currentPoint = user.optInt(UserExt.USER_POINT);
+
+ if (currentPoint - point < 0) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, langPropsService.get("insufficientBalanceLabel"));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final String memo = context.param(Common.MEMO);
+
+ final String transferId = pointtransferMgmtService.transfer(userId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_ABUSE_DEDUCT, point, memo, System.currentTimeMillis(), "");
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_DEDUCT_POINT, transferId));
+
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, userId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, transferId);
+ notificationMgmtService.addAbusePointDeductNotification(notification);
+ } catch (final Exception e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Compensates a user's initial point.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}/init-point", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void initPoint(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+ final Response response = context.getResponse();
+
+ try {
+ final JSONObject user = userQueryService.getUser(userId);
+ if (null == user
+ || UserExt.USER_STATUS_C_VALID != user.optInt(UserExt.USER_STATUS)
+ || UserExt.NULL_USER_NAME.equals(user.optString(User.USER_NAME))) {
+ response.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+
+ return;
+ }
+
+ final List records
+ = pointtransferQueryService.getLatestPointtransfers(userId, Pointtransfer.TRANSFER_TYPE_C_INIT, 1);
+ if (records.isEmpty()) {
+ final String transferId = pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId, Pointtransfer.TRANSFER_TYPE_C_INIT,
+ Pointtransfer.TRANSFER_SUM_C_INIT, userId, Long.valueOf(userId), "");
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_INIT_POINT, transferId));
+ }
+ } catch (final Exception e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Exchanges a user's point.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/user/{userId}/exchange-point", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void exchangePoint(final RequestContext context) {
+ final String userId = context.pathVar("userId");
+ final Request request = context.getRequest();
+ final String pointStr = context.param(Common.POINT);
+
+ try {
+ final int point = Integer.valueOf(pointStr);
+
+ final JSONObject user = userQueryService.getUser(userId);
+ final int currentPoint = user.optInt(UserExt.USER_POINT);
+
+ if (currentPoint - point < Symphonys.POINT_EXCHANGE_MIN) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, langPropsService.get("insufficientBalanceLabel"));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final String memo = String.valueOf(Math.floor(point / (double) Symphonys.POINT_EXCHANGE_UNIT));
+
+ final String transferId = pointtransferMgmtService.transfer(userId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_EXCHANGE, point, memo, System.currentTimeMillis(), "");
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_EXCHANGE_POINT, transferId));
+
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, userId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, transferId);
+ notificationMgmtService.addPointExchangeNotification(notification);
+ } catch (final Exception e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/user/" + userId);
+ }
+
+ /**
+ * Shows admin articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/articles", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showArticles(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/articles.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final String articleId = context.param("id");
+ if (StringUtils.isNotBlank(articleId)) {
+ requestJSONObject.put(Keys.OBJECT_ID, articleId);
+ }
+
+ final List articleFields = new ArrayList<>();
+ articleFields.add(Keys.OBJECT_ID);
+ articleFields.add(Article.ARTICLE_TITLE);
+ articleFields.add(Article.ARTICLE_PERMALINK);
+ articleFields.add(Article.ARTICLE_CREATE_TIME);
+ articleFields.add(Article.ARTICLE_VIEW_CNT);
+ articleFields.add(Article.ARTICLE_COMMENT_CNT);
+ articleFields.add(Article.ARTICLE_AUTHOR_ID);
+ articleFields.add(Article.ARTICLE_TAGS);
+ articleFields.add(Article.ARTICLE_STATUS);
+ articleFields.add(Article.ARTICLE_STICK);
+ final JSONObject result = articleQueryService.getArticles(requestJSONObject, articleFields);
+ dataModel.put(Article.ARTICLES, CollectionUtils.jsonArrayToList(result.optJSONArray(Article.ARTICLES)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/article/{articleId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showArticle(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/article.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String articleId = context.pathVar("articleId");
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ Escapes.escapeHTML(article);
+ dataModel.put(Article.ARTICLE, article);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/article/{articleId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateArticle(final RequestContext context) {
+ final String articleId = context.pathVar("articleId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/article.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ JSONObject article = articleQueryService.getArticle(articleId);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+ if (name.equals(Article.ARTICLE_REWARD_POINT)
+ || name.equals(Article.ARTICLE_QNA_OFFER_POINT)
+ || name.equals(Article.ARTICLE_STATUS)
+ || name.equals(Article.ARTICLE_TYPE)
+ || name.equals(Article.ARTICLE_THANK_CNT)
+ || name.equals(Article.ARTICLE_GOOD_CNT)
+ || name.equals(Article.ARTICLE_BAD_CNT)
+ || name.equals(Article.ARTICLE_PERFECT)
+ || name.equals(Article.ARTICLE_ANONYMOUS_VIEW)
+ || name.equals(Article.ARTICLE_PUSH_ORDER)
+ || name.equals(Article.ARTICLE_SHOW_IN_LIST)) {
+ article.put(name, Integer.valueOf(value));
+ } else {
+ article.put(name, value);
+ }
+ }
+
+ final String articleTags = Tag.formatTags(article.optString(Article.ARTICLE_TAGS));
+ article.put(Article.ARTICLE_TAGS, articleTags);
+
+ articleMgmtService.updateArticleByAdmin(articleId, article);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_ARTICLE, articleId));
+
+ article = articleQueryService.getArticle(articleId);
+ String title = article.optString(Article.ARTICLE_TITLE);
+ title = Escapes.escapeHTML(title);
+ article.put(Article.ARTICLE_TITLE, title);
+ dataModel.put(Article.ARTICLE, article);
+
+ updateArticleSearchIndex(article);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows admin comments.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/comments", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showComments(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/comments.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final List commentFields = new ArrayList<>();
+ commentFields.add(Keys.OBJECT_ID);
+ commentFields.add(Comment.COMMENT_CREATE_TIME);
+ commentFields.add(Comment.COMMENT_AUTHOR_ID);
+ commentFields.add(Comment.COMMENT_ON_ARTICLE_ID);
+ commentFields.add(Comment.COMMENT_SHARP_URL);
+ commentFields.add(Comment.COMMENT_STATUS);
+ commentFields.add(Comment.COMMENT_CONTENT);
+ final JSONObject result = commentQueryService.getComments(requestJSONObject, commentFields);
+ dataModel.put(Comment.COMMENTS, CollectionUtils.jsonArrayToList(result.optJSONArray(Comment.COMMENTS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows a comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/comment/{commentId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showComment(final RequestContext context) {
+ final String commentId = context.pathVar("commentId");
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/comment.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject comment = commentQueryService.getComment(commentId);
+ Escapes.escapeHTML(comment);
+ dataModel.put(Comment.COMMENT, comment);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates a comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/comment/{commentId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateComment(final RequestContext context) {
+ final String commentId = context.pathVar("commentId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/comment.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ JSONObject comment = commentQueryService.getComment(commentId);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ if (name.equals(Comment.COMMENT_STATUS)
+ || name.equals(Comment.COMMENT_THANK_CNT)
+ || name.equals(Comment.COMMENT_GOOD_CNT)
+ || name.equals(Comment.COMMENT_BAD_CNT)) {
+ comment.put(name, Integer.valueOf(value));
+ } else {
+ comment.put(name, value);
+ }
+ }
+
+ commentMgmtService.updateCommentByAdmin(commentId, comment);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_COMMENT, commentId));
+
+ comment = commentQueryService.getComment(commentId);
+ dataModel.put(Comment.COMMENT, comment);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows admin miscellaneous.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/misc", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showMisc(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/misc.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final List misc = optionQueryService.getMisc();
+ dataModel.put(Option.OPTIONS, misc);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates admin miscellaneous.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/misc", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateMisc(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/misc.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ List misc = new ArrayList<>();
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ final JSONObject option = new JSONObject();
+ option.put(Keys.OBJECT_ID, name);
+ option.put(Option.OPTION_VALUE, value);
+ option.put(Option.OPTION_CATEGORY, Option.CATEGORY_C_MISC);
+
+ misc.add(option);
+ }
+
+ for (final JSONObject option : misc) {
+ optionMgmtService.updateOption(option.getString(Keys.OBJECT_ID), option);
+ }
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_MISC, ""));
+
+ misc = optionQueryService.getMisc();
+ dataModel.put(Option.OPTIONS, misc);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows admin tags.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/tags", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showTags(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/tags.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final String tagTitle = context.param(Common.TITLE);
+ if (StringUtils.isNotBlank(tagTitle)) {
+ requestJSONObject.put(Tag.TAG_TITLE, tagTitle);
+ }
+
+ final List tagFields = new ArrayList<>();
+ tagFields.add(Keys.OBJECT_ID);
+ tagFields.add(Tag.TAG_TITLE);
+ tagFields.add(Tag.TAG_DESCRIPTION);
+ tagFields.add(Tag.TAG_ICON_PATH);
+ tagFields.add(Tag.TAG_COMMENT_CNT);
+ tagFields.add(Tag.TAG_REFERENCE_CNT);
+ tagFields.add(Tag.TAG_FOLLOWER_CNT);
+ tagFields.add(Tag.TAG_STATUS);
+ tagFields.add(Tag.TAG_GOOD_CNT);
+ tagFields.add(Tag.TAG_BAD_CNT);
+ tagFields.add(Tag.TAG_URI);
+ tagFields.add(Tag.TAG_CSS);
+ final JSONObject result = tagQueryService.getTags(requestJSONObject, tagFields);
+ dataModel.put(Tag.TAGS, CollectionUtils.jsonArrayToList(result.optJSONArray(Tag.TAGS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows a tag.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/tag/{tagId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showTag(final RequestContext context) {
+ final String tagId = context.pathVar("tagId");
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/tag.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject tag = tagQueryService.getTag(tagId);
+ if (null == tag) {
+ context.setRenderer(renderer);
+ renderer.setTemplateName("admin/error.ftl");
+
+ dataModel.put(Keys.MSG, langPropsService.get("notFoundTagLabel"));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ dataModel.put(Tag.TAG, tag);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Updates a tag.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/tag/{tagId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateTag(final RequestContext context) {
+ final String tagId = context.pathVar("tagId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/tag.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ JSONObject tag = tagQueryService.getTag(tagId);
+
+ final String oldTitle = tag.optString(Tag.TAG_TITLE);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ final String value = context.param(name);
+
+ if (name.equals(Tag.TAG_REFERENCE_CNT)
+ || name.equals(Tag.TAG_COMMENT_CNT)
+ || name.equals(Tag.TAG_FOLLOWER_CNT)
+ || name.contains(Tag.TAG_LINK_CNT)
+ || name.contains(Tag.TAG_STATUS)
+ || name.equals(Tag.TAG_GOOD_CNT)
+ || name.equals(Tag.TAG_BAD_CNT)
+ || name.equals(Tag.TAG_SHOW_SIDE_AD)) {
+ tag.put(name, Integer.valueOf(value));
+ } else {
+ tag.put(name, value);
+ }
+ }
+
+ final String newTitle = tag.optString(Tag.TAG_TITLE);
+
+ if (oldTitle.equalsIgnoreCase(newTitle)) {
+ try {
+ tagMgmtService.updateTag(tagId, tag);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_TAG, tagId));
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Updates a tag failed", e);
+
+ return;
+ }
+ }
+
+ tag = tagQueryService.getTag(tagId);
+ dataModel.put(Tag.TAG, tag);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows admin domains.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/domains", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showDomains(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/domains.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = PAGE_SIZE;
+ final int windowSize = WINDOW_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+
+ final String domainTitle = context.param(Common.TITLE);
+ if (StringUtils.isNotBlank(domainTitle)) {
+ requestJSONObject.put(Domain.DOMAIN_TITLE, domainTitle);
+ }
+
+ final List domainFields = new ArrayList<>();
+ domainFields.add(Keys.OBJECT_ID);
+ domainFields.add(Domain.DOMAIN_TITLE);
+ domainFields.add(Domain.DOMAIN_DESCRIPTION);
+ domainFields.add(Domain.DOMAIN_ICON_PATH);
+ domainFields.add(Domain.DOMAIN_STATUS);
+ domainFields.add(Domain.DOMAIN_URI);
+ final JSONObject result = domainQueryService.getDomains(requestJSONObject, domainFields);
+ dataModel.put(Common.ALL_DOMAINS, CollectionUtils.jsonArrayToList(result.optJSONArray(Domain.DOMAINS)));
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final JSONArray pageNums = pagination.optJSONArray(Pagination.PAGINATION_PAGE_NUMS);
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.opt(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.opt(pageNums.length() - 1));
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, CollectionUtils.jsonArrayToList(pageNums));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows a domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/domain/{domainId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showDomain(final RequestContext context) {
+ final String domainId = context.pathVar("domainId");
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/domain.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject domain = domainQueryService.getDomain(domainId);
+ dataModel.put(Domain.DOMAIN, domain);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ }
+
+ /**
+ * Updates a domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/domain/{domainId}", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void updateDomain(final RequestContext context) {
+ final String domainId = context.pathVar("domainId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/domain.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ JSONObject domain = domainQueryService.getDomain(domainId);
+ final String oldTitle = domain.optString(Domain.DOMAIN_TITLE);
+
+ final Iterator parameterNames = request.getParameterNames().iterator();
+ while (parameterNames.hasNext()) {
+ final String name = parameterNames.next();
+ String value = context.param(name);
+
+ if (Domain.DOMAIN_ICON_PATH.equals(name)) {
+ value = StringUtils.replace(value, "\"", "'");
+ }
+
+ domain.put(name, value);
+ }
+
+ domain.remove(Domain.DOMAIN_T_TAGS);
+
+ final String newTitle = domain.optString(Domain.DOMAIN_TITLE);
+
+ if (oldTitle.equalsIgnoreCase(newTitle)) {
+ domainMgmtService.updateDomain(domainId, domain);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_UPDATE_DOMAIN, domainId));
+ }
+
+ domain = domainQueryService.getDomain(domainId);
+ dataModel.put(Domain.DOMAIN, domain);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows add domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-domain", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAddDomain(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/add-domain.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Adds a domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/add-domain", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void addDomain(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String domainTitle = context.param(Domain.DOMAIN_TITLE);
+
+ if (StringUtils.isBlank(domainTitle)) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, langPropsService.get("invalidDomainTitleLabel"));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ if (null != domainQueryService.getByTitle(domainTitle)) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, langPropsService.get("duplicatedDomainLabel"));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ String domainId;
+ try {
+ final JSONObject domain = new JSONObject();
+ domain.put(Domain.DOMAIN_TITLE, domainTitle);
+ domain.put(Domain.DOMAIN_URI, domainTitle);
+ domain.put(Domain.DOMAIN_DESCRIPTION, domainTitle);
+ domain.put(Domain.DOMAIN_STATUS, Domain.DOMAIN_STATUS_C_VALID);
+
+ domainId = domainMgmtService.addDomain(domain);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_DOMAIN, domainId));
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/domain/" + domainId);
+ }
+
+ /**
+ * Removes a domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/remove-domain", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeDomain(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final String domainId = context.param(Domain.DOMAIN_T_ID);
+ domainMgmtService.removeDomain(domainId);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_DOMAIN, domainId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/domains");
+ }
+
+ /**
+ * Adds a tag into a domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/domain/{domainId}/add-tag", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void addDomainTag(final RequestContext context) {
+ final String domainId = context.pathVar("domainId");
+ final Request request = context.getRequest();
+
+ String tagTitle = context.param(Tag.TAG_TITLE);
+ final JSONObject tag = tagQueryService.getTagByTitle(tagTitle);
+
+ String tagId;
+ if (tag != null) {
+ tagId = tag.optString(Keys.OBJECT_ID);
+ } else {
+ try {
+ if (StringUtils.isBlank(tagTitle)) {
+ throw new Exception(langPropsService.get("tagsErrorLabel"));
+ }
+
+ tagTitle = Tag.formatTags(tagTitle);
+
+ if (!Tag.containsWhiteListTags(tagTitle)) {
+ if (!Tag.TAG_TITLE_PATTERN.matcher(tagTitle).matches()) {
+ throw new Exception(langPropsService.get("tagsErrorLabel"));
+ }
+
+ if (tagTitle.length() > Tag.MAX_TAG_TITLE_LENGTH) {
+ throw new Exception(langPropsService.get("tagsErrorLabel"));
+ }
+ }
+ } catch (final Exception e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final JSONObject admin = Sessions.getUser();
+ final String userId = admin.optString(Keys.OBJECT_ID);
+
+ try {
+ tagId = tagMgmtService.addTag(userId, tagTitle);
+ } catch (final ServiceException e) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, e.getMessage());
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+ }
+
+ if (domainQueryService.containTag(tagTitle, domainId)) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ String msg = langPropsService.get("domainContainTagLabel");
+ msg = msg.replace("{tag}", tagTitle);
+
+ dataModel.put(Keys.MSG, msg);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final JSONObject domainTag = new JSONObject();
+ domainTag.put(Domain.DOMAIN + "_" + Keys.OBJECT_ID, domainId);
+ domainTag.put(Tag.TAG + "_" + Keys.OBJECT_ID, tagId);
+
+ domainMgmtService.addDomainTag(domainTag);
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_ADD_DOMAIN_TAG, domainId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/domain/" + domainId);
+ }
+
+ /**
+ * Removes a tag from a domain.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/domain/{domainId}/remove-tag", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void removeDomainTag(final RequestContext context) {
+ final String domainId = context.pathVar("domainId");
+ final Request request = context.getRequest();
+
+ final String tagTitle = context.param(Tag.TAG_TITLE);
+ final JSONObject tag = tagQueryService.getTagByTitle(tagTitle);
+
+ if (null == tag) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "admin/error.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put(Keys.MSG, langPropsService.get("invalidTagLabel"));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ return;
+ }
+
+ final JSONObject domainTag = new JSONObject();
+ domainTag.put(Domain.DOMAIN + "_" + Keys.OBJECT_ID, domainId);
+ domainTag.put(Tag.TAG + "_" + Keys.OBJECT_ID, tag.optString(Keys.OBJECT_ID));
+
+ domainMgmtService.removeDomainTag(domainId, tag.optString(Keys.OBJECT_ID));
+ operationMgmtService.addOperation(Operation.newOperation(request, Operation.OPERATION_CODE_C_REMOVE_DOMAIN_TAG, domainId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/domain/" + domainId);
+ }
+
+ /**
+ * Rebuilds article search index.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/search/index", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void rebuildArticleSearchIndex(final RequestContext context) {
+ context.renderJSON(true);
+
+ if (Symphonys.ES_ENABLED) {
+ searchMgmtService.rebuildESIndex();
+ }
+
+ if (Symphonys.ALGOLIA_ENABLED) {
+ searchMgmtService.rebuildAlgoliaIndex();
+ }
+
+ Symphonys.EXECUTOR_SERVICE.submit(() -> {
+ try {
+ final JSONObject stat = optionQueryService.getStatistic();
+ final int articleCount = stat.optInt(Option.ID_C_STATISTIC_ARTICLE_COUNT);
+ final int pages = (int) Math.ceil((double) articleCount / 50.0);
+
+ for (int pageNum = 1; pageNum <= pages; pageNum++) {
+ final List articles = articleQueryService.getValidArticles(pageNum, 50, Article.ARTICLE_TYPE_C_NORMAL, Article.ARTICLE_TYPE_C_CITY_BROADCAST);
+
+ for (final JSONObject article : articles) {
+ if (Symphonys.ALGOLIA_ENABLED) {
+ searchMgmtService.updateAlgoliaDocument(article);
+ }
+
+ if (Symphonys.ES_ENABLED) {
+ searchMgmtService.updateESDocument(article, Article.ARTICLE);
+ }
+ }
+
+ LOGGER.info("Indexed page [" + pageNum + "]");
+ }
+
+ LOGGER.info("Index finished");
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Search index failed", e);
+ }
+ });
+
+ operationMgmtService.addOperation(Operation.newOperation(context.getRequest(), Operation.OPERATION_CODE_C_REBUILD_ARTICLES_SEARCH, ""));
+ }
+
+ /**
+ * Rebuilds one article search index.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/admin/search-index-article", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void rebuildOneArticleSearchIndex(final RequestContext context) {
+ final String articleId = context.getRequest().getParameter(Article.ARTICLE_T_ID);
+ final JSONObject article = articleQueryService.getArticle(articleId);
+
+ updateArticleSearchIndex(article);
+ operationMgmtService.addOperation(Operation.newOperation(context.getRequest(), Operation.OPERATION_CODE_C_REBUILD_ARTICLE_SEARCH, articleId));
+
+ context.sendRedirect(Latkes.getServePath() + "/admin/articles");
+ }
+
+ private void updateArticleSearchIndex(final JSONObject article) {
+ if (null == article || Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)
+ || Article.ARTICLE_TYPE_C_THOUGHT == article.optInt(Article.ARTICLE_TYPE)) {
+ return;
+ }
+
+ if (Symphonys.ALGOLIA_ENABLED) {
+ searchMgmtService.updateAlgoliaDocument(article);
+ }
+
+ if (Symphonys.ES_ENABLED) {
+ searchMgmtService.updateESDocument(article, Article.ARTICLE);
+ }
+
+ final String articlePermalink = Latkes.getServePath() + article.optString(Article.ARTICLE_PERMALINK);
+ ArticleBaiduSender.sendToBaidu(articlePermalink);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/AfterRequestHandler.java b/src/main/java/org/b3log/symphony/processor/AfterRequestHandler.java
new file mode 100644
index 000000000..5037b4201
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/AfterRequestHandler.java
@@ -0,0 +1,72 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.handler.Handler;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.jdbc.JdbcRepository;
+import org.b3log.latke.util.Locales;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+
+import java.util.Map;
+
+/**
+ * After request handler.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Nov 3, 2019
+ * @since 3.6.0
+ */
+public class AfterRequestHandler implements Handler {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(AfterRequestHandler.class);
+
+ @Override
+ public void handle(final RequestContext context) {
+ Locales.setLocale(null);
+ Sessions.clearThreadLocalData();
+ JdbcRepository.dispose();
+
+ Stopwatchs.end();
+ final Request request = context.getRequest();
+ final long elapsed = Stopwatchs.getElapsed("Request initialized [" + request.getRequestURI() + "]");
+ final Map dataModel = context.getDataModel();
+ if (null != dataModel) {
+ dataModel.put(Common.ELAPSED, elapsed);
+ }
+ final int threshold = Symphonys.PERFORMANCE_THRESHOLD;
+ if (0 < threshold) {
+ if (elapsed >= threshold) {
+ LOGGER.log(Level.INFO, "Stopwatch: {0}{1}", Strings.LINE_SEPARATOR, Stopwatchs.getTimingStat());
+ }
+ }
+ Stopwatchs.release();
+ }
+
+
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ArticleProcessor.java b/src/main/java/org/b3log/symphony/processor/ArticleProcessor.java
new file mode 100644
index 000000000..fd24affb1
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ArticleProcessor.java
@@ -0,0 +1,1320 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import jodd.util.Base64;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.latke.util.Paginator;
+import org.b3log.latke.util.Requests;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.cache.DomainCache;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.*;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.ArticleAddValidation;
+import org.b3log.symphony.processor.advice.validate.ArticleUpdateValidation;
+import org.b3log.symphony.processor.advice.validate.UserRegisterValidation;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.util.List;
+import java.util.*;
+
+/**
+ * Article processor.
+ *
+ * Shows an article (/article/{articleId}), GET
+ * Shows article pre adding form page (/pre-post), GET
+ * Shows article adding form page (/post), GET
+ * Adds an article (/post) locally , POST
+ * Shows an article updating form page (/update) locally , GET
+ * Updates an article (/article/{id}) locally , PUT
+ * Markdowns text (/markdown), POST
+ * Rewards an article (/article/reward), POST
+ * Gets an article preview content (/article/{articleId}/preview), GET
+ * Sticks an article (/article/stick), POST
+ * Gets an article's revisions (/article/{id}/revisions), GET
+ * Gets article image (/article/{articleId}/image), GET
+ * Checks article title (/article/check-title), POST
+ * Removes an article (/article/{id}/remove), POST
+ *
+ *
+ * @author Liang Ding
+ * @author Zephyr
+ * @author qiankunpingtai
+ * @version 1.27.3.5, Sep 6, 2019
+ * @since 0.2.0
+ */
+@RequestProcessor
+public class ArticleProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleProcessor.class);
+
+ /**
+ * Revision query service.
+ */
+ @Inject
+ private RevisionQueryService revisionQueryService;
+
+ /**
+ * Short link query service.
+ */
+ @Inject
+ private ShortLinkQueryService shortLinkQueryService;
+
+ /**
+ * Article management service.
+ */
+ @Inject
+ private ArticleMgmtService articleMgmtService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Follow query service.
+ */
+ @Inject
+ private FollowQueryService followQueryService;
+
+ /**
+ * Reward query service.
+ */
+ @Inject
+ private RewardQueryService rewardQueryService;
+
+ /**
+ * Vote query service.
+ */
+ @Inject
+ private VoteQueryService voteQueryService;
+
+ /**
+ * Liveness management service.
+ */
+ @Inject
+ private LivenessMgmtService livenessMgmtService;
+
+ /**
+ * Referral management service.
+ */
+ @Inject
+ private ReferralMgmtService referralMgmtService;
+
+ /**
+ * Character query service.
+ */
+ @Inject
+ private CharacterQueryService characterQueryService;
+
+ /**
+ * Domain query service.
+ */
+ @Inject
+ private DomainQueryService domainQueryService;
+
+ /**
+ * Domain cache.
+ */
+ @Inject
+ private DomainCache domainCache;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Removes an article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article/{id}/remove", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, PermissionCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void removeArticle(final RequestContext context) {
+ final String id = context.pathVar("id");
+ if (StringUtils.isBlank(id)) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String currentUserId = currentUser.optString(Keys.OBJECT_ID);
+ final JSONObject article = articleQueryService.getArticle(id);
+ if (null == article) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+ if (!authorId.equals(currentUserId)) {
+ context.sendError(403);
+
+ return;
+ }
+
+ context.renderJSON();
+ try {
+ articleMgmtService.removeArticle(id);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ context.renderJSONValue(Article.ARTICLE_T_ID, id);
+ } catch (final ServiceException e) {
+ final String msg = e.getMessage();
+
+ context.renderMsg(msg);
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ /**
+ * Checks article title.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article/check-title", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void checkArticleTitle(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String currentUserId = currentUser.optString(Keys.OBJECT_ID);
+ final JSONObject requestJSONObject = context.requestJSON();
+ String title = requestJSONObject.optString(Article.ARTICLE_TITLE);
+ title = StringUtils.trim(title);
+ String id = requestJSONObject.optString(Article.ARTICLE_T_ID);
+
+ final JSONObject article = articleQueryService.getArticleByTitle(title);
+ if (null == article) {
+ context.renderJSON(true);
+
+ return;
+ }
+
+ if (StringUtils.isBlank(id)) { // Add
+ final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+
+ String msg;
+ if (authorId.equals(currentUserId)) {
+ msg = langPropsService.get("duplicatedArticleTitleSelfLabel");
+ msg = msg.replace("{article}", "" + title + " ");
+ } else {
+ final JSONObject author = userQueryService.getUser(authorId);
+ final String userName = author.optString(User.USER_NAME);
+
+ msg = langPropsService.get("duplicatedArticleTitleLabel");
+ msg = msg.replace("{user}", "" + userName + " ");
+ msg = msg.replace("{article}", "" + title + " ");
+ }
+
+ final JSONObject ret = new JSONObject();
+ ret.put(Keys.STATUS_CODE, false);
+ ret.put(Keys.MSG, msg);
+
+ context.renderJSON(ret);
+ } else { // Update
+ final JSONObject oldArticle = articleQueryService.getArticle(id);
+ if (oldArticle.optString(Article.ARTICLE_TITLE).equals(title)) {
+ context.renderJSON(true);
+
+ return;
+ }
+
+ final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+
+ String msg;
+ if (authorId.equals(currentUserId)) {
+ msg = langPropsService.get("duplicatedArticleTitleSelfLabel");
+ msg = msg.replace("{article}", "" + title + " ");
+ } else {
+ final JSONObject author = userQueryService.getUser(authorId);
+ final String userName = author.optString(User.USER_NAME);
+
+ msg = langPropsService.get("duplicatedArticleTitleLabel");
+ msg = msg.replace("{user}", "" + userName + " ");
+ msg = msg.replace("{article}", "" + title + " ");
+ }
+
+ final JSONObject ret = new JSONObject();
+ ret.put(Keys.STATUS_CODE, false);
+ ret.put(Keys.MSG, msg);
+
+ context.renderJSON(ret);
+ }
+ }
+
+ /**
+ * Gets article image.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article/{articleId}/image", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void getArticleImage(final RequestContext context) {
+ final String articleId = context.pathVar("articleId");
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ final String authorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+
+ final Set characters = characterQueryService.getWrittenCharacters();
+ final String articleContent = article.optString(Article.ARTICLE_CONTENT);
+
+ final List images = new ArrayList<>();
+ for (int i = 0; i < articleContent.length(); i++) {
+ final String ch = articleContent.substring(i, i + 1);
+ final JSONObject chRecord = org.b3log.symphony.model.Character.getCharacter(ch, characters);
+ if (null == chRecord) {
+ images.add(org.b3log.symphony.model.Character.createImage(ch));
+
+ continue;
+ }
+
+ final String imgData = chRecord.optString(org.b3log.symphony.model.Character.CHARACTER_IMG);
+ final byte[] data = Base64.decode(imgData.getBytes());
+ BufferedImage img;
+ try {
+ img = ImageIO.read(new ByteArrayInputStream(data));
+ final BufferedImage newImage = new BufferedImage(50, 50, img.getType());
+ final Graphics g = newImage.getGraphics();
+ g.setClip(0, 0, 50, 50);
+ g.fillRect(0, 0, 50, 50);
+ g.drawImage(img, 0, 0, 50, 50, null);
+ g.dispose();
+
+ images.add(newImage);
+ } catch (final Exception e) {
+ // ignored
+ }
+ }
+
+ final int rowCharacterCount = 30;
+ final int rows = (int) Math.ceil((double) images.size() / (double) rowCharacterCount);
+
+ final BufferedImage combined = new BufferedImage(30 * 50, rows * 50, Transparency.TRANSLUCENT);
+ int row = 0;
+ for (int i = 0; i < images.size(); i++) {
+ final BufferedImage image = images.get(i);
+
+ final Graphics g = combined.getGraphics();
+ g.drawImage(image, (i % rowCharacterCount) * 50, row * 50, null);
+
+ if (0 == (i + 1) % rowCharacterCount) {
+ row++;
+ }
+ }
+
+ try {
+ ImageIO.write(combined, "PNG", new File("./hp.png"));
+ } catch (final Exception e) {
+ // ignored
+ }
+
+ String url = "";
+
+ final JSONObject ret = new JSONObject();
+ ret.put(Keys.STATUS_CODE, true);
+ ret.put(Common.URL, url);
+
+ context.renderJSON(ret);
+ }
+
+ /**
+ * Gets an article's revisions.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article/{id}/revisions", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, PermissionCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void getArticleRevisions(final RequestContext context) {
+ final String id = context.pathVar("id");
+ final List revisions = revisionQueryService.getArticleRevisions(id);
+ final JSONObject ret = new JSONObject();
+ ret.put(Keys.STATUS_CODE, true);
+ ret.put(Revision.REVISIONS, (Object) revisions);
+
+ context.renderJSON(ret);
+ }
+
+ /**
+ * Shows pre-add article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/pre-post", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showPreAddArticle(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/pre-post.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Common.BROADCAST_POINT, Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST);
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Fills the domains with tags.
+ *
+ * @param dataModel the specified data model
+ */
+ private void fillDomainsWithTags(final Map dataModel) {
+ final List domains = domainQueryService.getAllDomains();
+ dataModel.put(Common.ADD_ARTICLE_DOMAINS, domains);
+ for (final JSONObject domain : domains) {
+ final List tags = domainQueryService.getTags(domain.optString(Keys.OBJECT_ID));
+
+ domain.put(Domain.DOMAIN_T_TAGS, (Object) tags);
+ }
+
+ final JSONObject user = Sessions.getUser();
+ if (null == user) {
+ return;
+ }
+
+ try {
+ final JSONObject followingTagsResult = followQueryService.getFollowingTags(
+ user.optString(Keys.OBJECT_ID), 1, 28);
+ final List followingTags = (List) followingTagsResult.opt(Keys.RESULTS);
+ if (!followingTags.isEmpty()) {
+ final JSONObject userWatched = new JSONObject();
+ userWatched.put(Keys.OBJECT_ID, String.valueOf(System.currentTimeMillis()));
+ userWatched.put(Domain.DOMAIN_TITLE, langPropsService.get("notificationFollowingLabel"));
+ userWatched.put(Domain.DOMAIN_T_TAGS, (Object) followingTags);
+
+ domains.add(0, userWatched);
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Get user [name=" + user.optString(User.USER_NAME) + "] following tags failed", e);
+ }
+ }
+
+ /**
+ * Shows add article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/post", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAddArticle(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/post.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ String tags = context.param(Tag.TAGS);
+ final JSONObject currentUser = Sessions.getUser();
+
+ if (StringUtils.isBlank(tags)) {
+ tags = "";
+
+ dataModel.put(Tag.TAGS, tags);
+ } else {
+ tags = Tag.formatTags(tags);
+ final String[] tagTitles = tags.split(",");
+
+ final StringBuilder tagBuilder = new StringBuilder();
+ for (final String title : tagTitles) {
+ final String tagTitle = title.trim();
+
+ if (StringUtils.isBlank(tagTitle)) {
+ continue;
+ }
+
+ if (Tag.containsWhiteListTags(tagTitle)) {
+ tagBuilder.append(tagTitle).append(",");
+
+ continue;
+ }
+
+ if (!Tag.TAG_TITLE_PATTERN.matcher(tagTitle).matches()) {
+ continue;
+ }
+
+ if (tagTitle.length() > Tag.MAX_TAG_TITLE_LENGTH) {
+ continue;
+ }
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && ArrayUtils.contains(Symphonys.RESERVED_TAGS, tagTitle)) {
+ continue;
+ }
+
+ tagBuilder.append(tagTitle).append(",");
+ }
+ if (tagBuilder.length() > 0) {
+ tagBuilder.deleteCharAt(tagBuilder.length() - 1);
+ }
+
+ dataModel.put(Tag.TAGS, tagBuilder.toString());
+ }
+
+ final String type = context.param(Common.TYPE);
+ if (StringUtils.isBlank(type)) {
+ dataModel.put(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
+ } else {
+ int articleType = Article.ARTICLE_TYPE_C_NORMAL;
+
+ try {
+ articleType = Integer.valueOf(type);
+ } catch (final Exception e) {
+ LOGGER.log(Level.WARN, "Gets article type error [" + type + "]", e);
+ }
+
+ if (Article.isInvalidArticleType(articleType)) {
+ articleType = Article.ARTICLE_TYPE_C_NORMAL;
+ }
+
+ dataModel.put(Article.ARTICLE_TYPE, articleType);
+ }
+
+ String at = context.param(Common.AT);
+ at = StringUtils.trim(at);
+ if (StringUtils.isNotBlank(at)) {
+ dataModel.put(Common.AT, at + " ");
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ String rewardEditorPlaceholderLabel = langPropsService.get("rewardEditorPlaceholderLabel");
+ rewardEditorPlaceholderLabel = rewardEditorPlaceholderLabel.replace("{point}",
+ String.valueOf(Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_REWARD));
+ dataModel.put("rewardEditorPlaceholderLabel", rewardEditorPlaceholderLabel);
+ dataModel.put(Common.BROADCAST_POINT, Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST);
+
+ String articleContentErrorLabel = langPropsService.get("articleContentErrorLabel");
+ articleContentErrorLabel = articleContentErrorLabel.replace("{maxArticleContentLength}",
+ String.valueOf(ArticleAddValidation.MAX_ARTICLE_CONTENT_LENGTH));
+ dataModel.put("articleContentErrorLabel", articleContentErrorLabel);
+
+ fillPostArticleRequisite(dataModel, currentUser);
+ fillDomainsWithTags(dataModel);
+ }
+
+ private void fillPostArticleRequisite(final Map dataModel, final JSONObject currentUser) {
+ boolean requisite = false;
+ String requisiteMsg = "";
+
+ dataModel.put(Common.REQUISITE, requisite);
+ dataModel.put(Common.REQUISITE_MSG, requisiteMsg);
+ }
+
+ /**
+ * Shows article with the specified article id.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article/{articleId}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showArticle(final RequestContext context) {
+ final String articleId = context.pathVar("articleId");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "article.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject article = articleQueryService.getArticleById(articleId);
+ if (null == article) {
+ context.sendError(404);
+
+ return;
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+ final JSONObject author = userQueryService.getUser(articleAuthorId);
+ Escapes.escapeHTML(author);
+
+ if (Article.ARTICLE_ANONYMOUS_C_PUBLIC == article.optInt(Article.ARTICLE_ANONYMOUS)) {
+ article.put(Article.ARTICLE_T_AUTHOR_NAME, author.optString(User.USER_NAME));
+ article.put(Article.ARTICLE_T_AUTHOR_URL, author.optString(User.USER_URL));
+ article.put(Article.ARTICLE_T_AUTHOR_INTRO, author.optString(UserExt.USER_INTRO));
+ } else {
+ article.put(Article.ARTICLE_T_AUTHOR_NAME, UserExt.ANONYMOUS_USER_NAME);
+ article.put(Article.ARTICLE_T_AUTHOR_URL, "");
+ article.put(Article.ARTICLE_T_AUTHOR_INTRO, "");
+ }
+ dataModel.put(Article.ARTICLE, article);
+
+ article.put(Common.IS_MY_ARTICLE, false);
+ article.put(Article.ARTICLE_T_AUTHOR, author);
+ article.put(Common.REWARDED, false);
+ article.put(Common.REWARED_COUNT, rewardQueryService.rewardedCount(articleId, Reward.TYPE_C_ARTICLE));
+ article.put(Article.ARTICLE_REVISION_COUNT, revisionQueryService.count(articleId, Revision.DATA_TYPE_C_ARTICLE));
+
+ articleQueryService.processArticleContent(article);
+
+ String cmtViewModeStr = context.param("m");
+ JSONObject currentUser;
+ String currentUserId = null;
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ currentUser = Sessions.getUser();
+ currentUserId = currentUser.optString(Keys.OBJECT_ID);
+ final boolean isMyArticle = currentUserId.equals(articleAuthorId);
+ article.put(Common.IS_MY_ARTICLE, isMyArticle);
+
+ final boolean isFollowing = followQueryService.isFollowing(currentUserId, articleId, Follow.FOLLOWING_TYPE_C_ARTICLE);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+
+ final boolean isWatching = followQueryService.isFollowing(currentUserId, articleId, Follow.FOLLOWING_TYPE_C_ARTICLE_WATCH);
+ dataModel.put(Common.IS_WATCHING, isWatching);
+
+ final int articleVote = voteQueryService.isVoted(currentUserId, articleId);
+ article.put(Article.ARTICLE_T_VOTE, articleVote);
+
+ if (isMyArticle) {
+ article.put(Common.REWARDED, true);
+ } else {
+ article.put(Common.REWARDED, rewardQueryService.isRewarded(currentUserId, articleId, Reward.TYPE_C_ARTICLE));
+ }
+
+ if (StringUtils.isBlank(cmtViewModeStr) || !Strings.isNumeric(cmtViewModeStr)) {
+ cmtViewModeStr = currentUser.optString(UserExt.USER_COMMENT_VIEW_MODE);
+ }
+ } else if (StringUtils.isBlank(cmtViewModeStr) || !Strings.isNumeric(cmtViewModeStr)) {
+ cmtViewModeStr = "0";
+ }
+
+ final int cmtViewMode = Integer.valueOf(cmtViewModeStr);
+ dataModel.put(UserExt.USER_COMMENT_VIEW_MODE, cmtViewMode);
+
+ final JSONObject viewer = Sessions.getUser();
+ if (null != viewer) {
+ livenessMgmtService.incLiveness(viewer.optString(Keys.OBJECT_ID), Liveness.LIVENESS_PV);
+ }
+
+ if (!Sessions.isBot()) {
+ final long created = System.currentTimeMillis();
+ final long expired = DateUtils.addMonths(new Date(created), 1).getTime();
+ final String ip = Requests.getRemoteAddr(request);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+ final String referer = Headers.getHeader(request, "Referer", "");
+ final JSONObject visit = new JSONObject();
+ visit.put(Visit.VISIT_IP, ip);
+ visit.put(Visit.VISIT_CITY, "");
+ visit.put(Visit.VISIT_CREATED, created);
+ visit.put(Visit.VISIT_DEVICE_ID, "");
+ visit.put(Visit.VISIT_EXPIRED, expired);
+ visit.put(Visit.VISIT_REFERER_URL, referer);
+ visit.put(Visit.VISIT_UA, ua);
+ visit.put(Visit.VISIT_URL, "/article/" + articleId);
+ visit.put(Visit.VISIT_USER_ID, "");
+ if (null != viewer) {
+ visit.put(Visit.VISIT_USER_ID, viewer.optString(Keys.OBJECT_ID));
+ }
+
+ articleMgmtService.incArticleViewCount(visit);
+ }
+
+ dataModelService.fillRelevantArticles(dataModel, article);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+
+ // Fill article thank
+ Stopwatchs.start("Fills article thank");
+ try {
+ article.put(Common.THANKED, rewardQueryService.isRewarded(currentUserId, articleId, Reward.TYPE_C_THANK_ARTICLE));
+ article.put(Common.THANKED_COUNT, article.optInt(Article.ARTICLE_THANK_CNT));
+ if (Article.ARTICLE_TYPE_C_QNA == article.optInt(Article.ARTICLE_TYPE)) {
+ article.put(Common.OFFERED, rewardQueryService.isRewarded(articleAuthorId, articleId, Reward.TYPE_C_ACCEPT_COMMENT));
+ final JSONObject offeredComment = commentQueryService.getOfferedComment(cmtViewMode, articleId);
+ article.put(Article.ARTICLE_T_OFFERED_COMMENT, offeredComment);
+ if (null != offeredComment) {
+ if (Comment.COMMENT_VISIBLE_C_AUTHOR == offeredComment.optInt(Comment.COMMENT_VISIBLE)) {
+ final String commentAuthorId = offeredComment.optString(Comment.COMMENT_AUTHOR_ID);
+ if (!isLoggedIn || (!StringUtils.equals(currentUserId, commentAuthorId) && !StringUtils.equals(currentUserId, articleAuthorId))) {
+ offeredComment.put(Comment.COMMENT_CONTENT, langPropsService.get("onlySelfAndArticleAuthorVisibleLabel"));
+ }
+ }
+ final String offeredCmtId = offeredComment.optString(Keys.OBJECT_ID);
+ final int rewardCount = offeredComment.optInt(Comment.COMMENT_THANK_CNT);
+ offeredComment.put(Common.REWARED_COUNT, rewardCount);
+ offeredComment.put(Common.REWARDED, rewardQueryService.isRewarded(currentUserId, offeredCmtId, Reward.TYPE_C_COMMENT));
+ }
+ }
+ } finally {
+ Stopwatchs.end();
+ }
+
+ // Fill previous/next article
+ final JSONObject previous = articleQueryService.getPreviousPermalink(articleId);
+ final JSONObject next = articleQueryService.getNextPermalink(articleId);
+ dataModel.put(Article.ARTICLE_T_PREVIOUS, previous);
+ dataModel.put(Article.ARTICLE_T_NEXT, next);
+
+ String stickConfirmLabel = langPropsService.get("stickConfirmLabel");
+ stickConfirmLabel = stickConfirmLabel.replace("{point}", Symphonys.POINT_STICK_ARTICLE + "");
+ dataModel.put("stickConfirmLabel", stickConfirmLabel);
+ dataModel.put("pointThankArticle", Symphonys.POINT_THANK_ARTICLE);
+
+ int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.ARTICLE_COMMENTS_CNT;
+ final int windowSize = Symphonys.ARTICLE_COMMENTS_WIN_SIZE;
+ final int commentCnt = article.getInt(Article.ARTICLE_COMMENT_CNT);
+ final int pageCount = (int) Math.ceil((double) commentCnt / (double) pageSize);
+ // 回帖分页 SEO https://github.com/b3log/symphony/issues/813
+ if (UserExt.USER_COMMENT_VIEW_MODE_C_TRADITIONAL == cmtViewMode) {
+ if (0 < pageCount && pageNum > pageCount) {
+ pageNum = pageCount;
+ }
+ } else {
+ if (pageNum > pageCount) {
+ pageNum = 1;
+ }
+ }
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Common.ARTICLE_COMMENTS_PAGE_SIZE, pageSize);
+
+ dataModel.put(Common.DISCUSSION_VIEWABLE, article.optBoolean(Common.DISCUSSION_VIEWABLE));
+ if (!article.optBoolean(Common.DISCUSSION_VIEWABLE)) {
+ article.put(Article.ARTICLE_T_COMMENTS, (Object) Collections.emptyList());
+ article.put(Article.ARTICLE_T_NICE_COMMENTS, (Object) Collections.emptyList());
+
+ return;
+ }
+
+ final List niceComments = commentQueryService.getNiceComments(cmtViewMode, articleId, 3);
+ article.put(Article.ARTICLE_T_NICE_COMMENTS, (Object) niceComments);
+
+ double niceCmtScore = Double.MAX_VALUE;
+ if (!niceComments.isEmpty()) {
+ niceCmtScore = niceComments.get(niceComments.size() - 1).optDouble(Comment.COMMENT_SCORE, 0D);
+
+ for (final JSONObject comment : niceComments) {
+ String thankTemplate = langPropsService.get("thankConfirmLabel");
+ thankTemplate = thankTemplate.replace("{point}", String.valueOf(Symphonys.POINT_THANK_COMMENT))
+ .replace("{user}", comment.optJSONObject(Comment.COMMENT_T_COMMENTER).optString(User.USER_NAME));
+ comment.put(Comment.COMMENT_T_THANK_LABEL, thankTemplate);
+
+ final String commentId = comment.optString(Keys.OBJECT_ID);
+ if (isLoggedIn) {
+ comment.put(Common.REWARDED, rewardQueryService.isRewarded(currentUserId, commentId, Reward.TYPE_C_COMMENT));
+ final int commentVote = voteQueryService.isVoted(currentUserId, commentId);
+ comment.put(Comment.COMMENT_T_VOTE, commentVote);
+ }
+
+ comment.put(Common.REWARED_COUNT, comment.optInt(Comment.COMMENT_THANK_CNT));
+
+ // https://github.com/b3log/symphony/issues/682
+ if (Comment.COMMENT_VISIBLE_C_AUTHOR == comment.optInt(Comment.COMMENT_VISIBLE)) {
+ final String commentAuthorId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+ if (!isLoggedIn || (!StringUtils.equals(currentUserId, commentAuthorId) && !StringUtils.equals(currentUserId, articleAuthorId))) {
+ comment.put(Comment.COMMENT_CONTENT, langPropsService.get("onlySelfAndArticleAuthorVisibleLabel"));
+ }
+ }
+ }
+ }
+
+ // Load comments
+ final List articleComments = commentQueryService.getArticleComments(articleId, pageNum, pageSize, cmtViewMode);
+ article.put(Article.ARTICLE_T_COMMENTS, (Object) articleComments);
+
+ // Fill comment thank
+ Stopwatchs.start("Fills comment thank");
+ try {
+ final String thankTemplate = langPropsService.get("thankConfirmLabel");
+ for (final JSONObject comment : articleComments) {
+ comment.put(Comment.COMMENT_T_NICE, comment.optDouble(Comment.COMMENT_SCORE, 0D) >= niceCmtScore);
+
+ final String thankStr = thankTemplate.replace("{point}", String.valueOf(Symphonys.POINT_THANK_COMMENT))
+ .replace("{user}", comment.optJSONObject(Comment.COMMENT_T_COMMENTER).optString(User.USER_NAME));
+ comment.put(Comment.COMMENT_T_THANK_LABEL, thankStr);
+
+ final String commentId = comment.optString(Keys.OBJECT_ID);
+ if (isLoggedIn) {
+ comment.put(Common.REWARDED,
+ rewardQueryService.isRewarded(currentUserId, commentId, Reward.TYPE_C_COMMENT));
+ final int commentVote = voteQueryService.isVoted(currentUserId, commentId);
+ comment.put(Comment.COMMENT_T_VOTE, commentVote);
+ }
+
+ comment.put(Common.REWARED_COUNT, comment.optInt(Comment.COMMENT_THANK_CNT));
+
+ // https://github.com/b3log/symphony/issues/682
+ if (Comment.COMMENT_VISIBLE_C_AUTHOR == comment.optInt(Comment.COMMENT_VISIBLE)) {
+ final String commentAuthorId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+ if (!isLoggedIn || (!StringUtils.equals(currentUserId, commentAuthorId) && !StringUtils.equals(currentUserId, articleAuthorId))) {
+ comment.put(Comment.COMMENT_CONTENT, langPropsService.get("onlySelfAndArticleAuthorVisibleLabel"));
+ }
+ }
+ }
+ } finally {
+ Stopwatchs.end();
+ }
+
+ // Referral statistic
+ final String referralUserName = context.param("r");
+ if (!UserRegisterValidation.invalidUserName(referralUserName)) {
+ final JSONObject referralUser = userQueryService.getUserByName(referralUserName);
+ if (null == referralUser) {
+ return;
+ }
+
+ final String viewerIP = Requests.getRemoteAddr(request);
+
+ final JSONObject referral = new JSONObject();
+ referral.put(Referral.REFERRAL_CLICK, 1);
+ referral.put(Referral.REFERRAL_DATA_ID, articleId);
+ referral.put(Referral.REFERRAL_IP, viewerIP);
+ referral.put(Referral.REFERRAL_TYPE, Referral.REFERRAL_TYPE_C_ARTICLE);
+ referral.put(Referral.REFERRAL_USER, referralUserName);
+
+ referralMgmtService.updateReferral(referral);
+ }
+
+ if (StringUtils.isBlank(article.optString(Article.ARTICLE_AUDIO_URL))) {
+ articleMgmtService.genArticleAudio(article);
+ }
+ }
+
+ /**
+ * Adds an article locally.
+ *
+ * The request json object (an article):
+ *
+ * {
+ * "articleTitle": "",
+ * "articleTags": "", // Tags spliting by ','
+ * "articleContent": "",
+ * "articleCommentable": boolean,
+ * "articleType": int,
+ * "articleRewardContent": "",
+ * "articleRewardPoint": int,
+ * "articleQnAOfferPoint": int,
+ * "articleAnonymous": boolean,
+ * "articleNotifyFollowers": boolean
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, ArticleAddValidation.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void addArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
+ String articleTags = requestJSONObject.optString(Article.ARTICLE_TAGS);
+ final String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
+ final boolean articleCommentable = requestJSONObject.optBoolean(Article.ARTICLE_COMMENTABLE, true);
+ final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
+ final String articleRewardContent = requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT);
+ final int articleRewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT);
+ final int articleQnAOfferPoint = requestJSONObject.optInt(Article.ARTICLE_QNA_OFFER_POINT);
+ final String ip = Requests.getRemoteAddr(request);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+ final boolean isAnonymous = requestJSONObject.optBoolean(Article.ARTICLE_ANONYMOUS, false);
+ final int articleAnonymous = isAnonymous ? Article.ARTICLE_ANONYMOUS_C_ANONYMOUS : Article.ARTICLE_ANONYMOUS_C_PUBLIC;
+ final boolean articleNotifyFollowers = requestJSONObject.optBoolean(Article.ARTICLE_T_NOTIFY_FOLLOWERS);
+ final Integer articleShowInList = requestJSONObject.optInt(Article.ARTICLE_SHOW_IN_LIST, Article.ARTICLE_SHOW_IN_LIST_C_YES);
+
+ final JSONObject article = new JSONObject();
+ article.put(Article.ARTICLE_TITLE, articleTitle);
+ article.put(Article.ARTICLE_CONTENT, articleContent);
+ article.put(Article.ARTICLE_EDITOR_TYPE, 0);
+ article.put(Article.ARTICLE_COMMENTABLE, articleCommentable);
+ article.put(Article.ARTICLE_TYPE, articleType);
+ article.put(Article.ARTICLE_REWARD_CONTENT, articleRewardContent);
+ article.put(Article.ARTICLE_REWARD_POINT, articleRewardPoint);
+ article.put(Article.ARTICLE_QNA_OFFER_POINT, articleQnAOfferPoint);
+ article.put(Article.ARTICLE_IP, "");
+ if (StringUtils.isNotBlank(ip)) {
+ article.put(Article.ARTICLE_IP, ip);
+ }
+ article.put(Article.ARTICLE_UA, ua);
+ article.put(Article.ARTICLE_ANONYMOUS, articleAnonymous);
+ article.put(Article.ARTICLE_T_NOTIFY_FOLLOWERS, articleNotifyFollowers);
+ article.put(Article.ARTICLE_SHOW_IN_LIST, articleShowInList);
+ try {
+ final JSONObject currentUser = Sessions.getUser();
+
+ article.put(Article.ARTICLE_AUTHOR_ID, currentUser.optString(Keys.OBJECT_ID));
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
+ articleTags = articleMgmtService.filterReservedTags(articleTags);
+ }
+
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == articleType && StringUtils.isBlank(articleTags)) {
+ articleTags = "机要";
+ }
+
+ if (Article.ARTICLE_TYPE_C_THOUGHT == articleType && StringUtils.isBlank(articleTags)) {
+ articleTags = "思绪";
+ }
+
+ article.put(Article.ARTICLE_TAGS, articleTags);
+
+ final String articleId = articleMgmtService.addArticle(article);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ context.renderJSONValue(Article.ARTICLE_T_ID, articleId);
+ } catch (final ServiceException e) {
+ final String msg = e.getMessage();
+ LOGGER.log(Level.ERROR, "Adds article [title=" + articleTitle + "] failed: {0}", e.getMessage());
+
+ context.renderMsg(msg);
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ /**
+ * Shows update article.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/update", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showUpdateArticle(final RequestContext context) {
+ final String articleId = context.param("id");
+ if (StringUtils.isBlank(articleId)) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ if (null == article) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser
+ || !currentUser.optString(Keys.OBJECT_ID).equals(article.optString(Article.ARTICLE_AUTHOR_ID))) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/post.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ String title = article.optString(Article.ARTICLE_TITLE);
+ title = Escapes.escapeHTML(title);
+ article.put(Article.ARTICLE_TITLE, title);
+ dataModel.put(Article.ARTICLE, article);
+ dataModel.put(Article.ARTICLE_TYPE, article.optInt(Article.ARTICLE_TYPE));
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ fillDomainsWithTags(dataModel);
+
+ String rewardEditorPlaceholderLabel = langPropsService.get("rewardEditorPlaceholderLabel");
+ rewardEditorPlaceholderLabel = rewardEditorPlaceholderLabel.replace("{point}",
+ String.valueOf(Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_REWARD));
+ dataModel.put("rewardEditorPlaceholderLabel", rewardEditorPlaceholderLabel);
+ dataModel.put(Common.BROADCAST_POINT, Pointtransfer.TRANSFER_SUM_C_ADD_ARTICLE_BROADCAST);
+
+ fillPostArticleRequisite(dataModel, currentUser);
+ }
+
+ /**
+ * Updates an article locally.
+ *
+ * The request json object (an article):
+ *
+ * {
+ * "articleTitle": "",
+ * "articleTags": "", // Tags spliting by ','
+ * "articleContent": "",
+ * "articleCommentable": boolean,
+ * "articleType": int,
+ * "articleRewardContent": "",
+ * "articleRewardPoint": int,
+ * "articleQnAOfferPoint": int,
+ * "articleNotifyFollowers": boolean
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/article/{id}", method = HttpMethod.PUT)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, ArticleUpdateValidation.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void updateArticle(final RequestContext context) {
+ final String id = context.pathVar("id");
+ final Request request = context.getRequest();
+ if (StringUtils.isBlank(id)) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONObject oldArticle = articleQueryService.getArticleById(id);
+ if (null == oldArticle) {
+ context.sendError(404);
+
+ return;
+ }
+
+ context.renderJSON();
+
+ if (Article.ARTICLE_STATUS_C_VALID != oldArticle.optInt(Article.ARTICLE_STATUS)) {
+ context.renderMsg(langPropsService.get("articleLockedLabel"));
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ return;
+ }
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
+ String articleTags = requestJSONObject.optString(Article.ARTICLE_TAGS);
+ final String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
+ final boolean articleCommentable = requestJSONObject.optBoolean(Article.ARTICLE_COMMENTABLE, true);
+ final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE, Article.ARTICLE_TYPE_C_NORMAL);
+ final String articleRewardContent = requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT);
+ final int articleRewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT);
+ final int articleQnAOfferPoint = requestJSONObject.optInt(Article.ARTICLE_QNA_OFFER_POINT);
+ final String ip = Requests.getRemoteAddr(request);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+ final boolean articleNotifyFollowers = requestJSONObject.optBoolean(Article.ARTICLE_T_NOTIFY_FOLLOWERS);
+ final Integer articleShowInList = requestJSONObject.optInt(Article.ARTICLE_SHOW_IN_LIST, Article.ARTICLE_SHOW_IN_LIST_C_YES);
+ final JSONObject article = new JSONObject();
+ article.put(Keys.OBJECT_ID, id);
+ article.put(Article.ARTICLE_TITLE, articleTitle);
+ article.put(Article.ARTICLE_CONTENT, articleContent);
+ article.put(Article.ARTICLE_EDITOR_TYPE, 0);
+ article.put(Article.ARTICLE_COMMENTABLE, articleCommentable);
+ article.put(Article.ARTICLE_TYPE, articleType);
+ article.put(Article.ARTICLE_REWARD_CONTENT, articleRewardContent);
+ article.put(Article.ARTICLE_REWARD_POINT, articleRewardPoint);
+ article.put(Article.ARTICLE_QNA_OFFER_POINT, articleQnAOfferPoint);
+ article.put(Article.ARTICLE_IP, "");
+ if (StringUtils.isNotBlank(ip)) {
+ article.put(Article.ARTICLE_IP, ip);
+ }
+ article.put(Article.ARTICLE_UA, ua);
+ article.put(Article.ARTICLE_T_NOTIFY_FOLLOWERS, articleNotifyFollowers);
+ article.put(Article.ARTICLE_SHOW_IN_LIST, articleShowInList);
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser
+ || !currentUser.optString(Keys.OBJECT_ID).equals(oldArticle.optString(Article.ARTICLE_AUTHOR_ID))) {
+ context.sendError(403);
+
+ return;
+ }
+
+ article.put(Article.ARTICLE_AUTHOR_ID, currentUser.optString(Keys.OBJECT_ID));
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
+ articleTags = articleMgmtService.filterReservedTags(articleTags);
+ }
+
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == articleType && StringUtils.isBlank(articleTags)) {
+ articleTags = "机要";
+ }
+
+ if (Article.ARTICLE_TYPE_C_THOUGHT == articleType && StringUtils.isBlank(articleTags)) {
+ articleTags = "思绪";
+ }
+
+ article.put(Article.ARTICLE_TAGS, articleTags);
+
+ try {
+ articleMgmtService.updateArticle(article);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ context.renderJSONValue(Article.ARTICLE_T_ID, id);
+ } catch (final ServiceException e) {
+ final String msg = e.getMessage();
+ LOGGER.log(Level.ERROR, "Adds article [title=" + articleTitle + "] failed: {0}", e.getMessage());
+
+ context.renderMsg(msg);
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ /**
+ * Markdowns.
+ *
+ * Renders the response with a json object, for example,
+ *
+ * {
+ * "html": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified http request context
+ */
+ @RequestProcessing(value = "/markdown", method = HttpMethod.POST)
+ @Before(StopwatchStartAdvice.class)
+ @After(StopwatchEndAdvice.class)
+ public void markdown2HTML(final RequestContext context) {
+ final JSONObject result = Results.newSucc();
+ context.renderJSON(result);
+
+ final JSONObject requestJSON = context.requestJSON();
+ final String markdownText = requestJSON.optString("markdownText");
+ if (StringUtils.isBlank(markdownText)) {
+ context.renderJSONValue("html", "");
+
+ return;
+ }
+
+ String html = shortLinkQueryService.linkArticle(markdownText);
+ html = Emotions.toAliases(html);
+ html = Emotions.convert(html);
+ html = Markdowns.toHTML(html);
+ html = Markdowns.clean(html, "");
+ html = MP3Players.render(html);
+ html = VideoPlayers.render(html);
+
+ result.put(Common.DATA, html);
+ }
+
+ /**
+ * Gets article preview content.
+ *
+ * Renders the response with a json object, for example,
+ *
+ * {
+ * "html": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified http request context
+ */
+ @RequestProcessing(value = "/article/{articleId}/preview", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After(StopwatchEndAdvice.class)
+ public void getArticlePreviewContent(final RequestContext context) {
+ final String articleId = context.pathVar("articleId");
+ final Request request = context.getRequest();
+ final String content = articleQueryService.getArticlePreviewContent(articleId, context);
+ if (StringUtils.isBlank(content)) {
+ context.renderJSON().renderFalseResult();
+
+ return;
+ }
+
+ context.renderJSON(true).renderJSONValue("html", content);
+ }
+
+ /**
+ * Article rewards.
+ *
+ * @param context the specified http request context
+ */
+ @RequestProcessing(value = "/article/reward", method = HttpMethod.POST)
+ @Before(StopwatchStartAdvice.class)
+ @After(StopwatchEndAdvice.class)
+ public void reward(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final String articleId = context.param(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(articleId)) {
+ context.sendError(400);
+
+ return;
+ }
+
+ context.renderJSON();
+
+ try {
+ articleMgmtService.reward(articleId, currentUser.optString(Keys.OBJECT_ID));
+ } catch (final ServiceException e) {
+ context.renderMsg(langPropsService.get("transferFailLabel"));
+
+ return;
+ }
+
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ articleQueryService.processArticleContent(article);
+
+ final String rewardContent = article.optString(Article.ARTICLE_REWARD_CONTENT);
+ context.renderTrueResult().renderJSONValue(Article.ARTICLE_REWARD_CONTENT, rewardContent);
+ }
+
+ /**
+ * Article thanks.
+ *
+ * @param context the specified http request context
+ */
+ @RequestProcessing(value = "/article/thank", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void thank(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final String articleId = context.param(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(articleId)) {
+ context.sendError(400);
+
+ return;
+ }
+
+ context.renderJSON();
+
+ try {
+ articleMgmtService.thank(articleId, currentUser.optString(Keys.OBJECT_ID));
+ } catch (final ServiceException e) {
+ context.renderMsg(langPropsService.get("transferFailLabel"));
+
+ return;
+ }
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Sticks an article.
+ *
+ * @param context the specified HTTP request context
+ */
+ @RequestProcessing(value = "/article/stick", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void stickArticle(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final String articleId = context.param(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(articleId)) {
+ context.sendError(400);
+
+ return;
+ }
+
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ if (null == article) {
+ context.sendError(404);
+
+ return;
+ }
+
+ if (!currentUser.optString(Keys.OBJECT_ID).equals(article.optString(Article.ARTICLE_AUTHOR_ID))) {
+ context.sendError(403);
+
+ return;
+ }
+
+ context.renderJSON();
+
+ try {
+ articleMgmtService.stick(articleId);
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+
+ return;
+ }
+
+ context.renderTrueResult().renderMsg(langPropsService.get("stickSuccLabel"));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/BeforeRequestHandler.java b/src/main/java/org/b3log/symphony/processor/BeforeRequestHandler.java
new file mode 100644
index 000000000..d067085a1
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/BeforeRequestHandler.java
@@ -0,0 +1,164 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import eu.bitwalker.useragentutils.BrowserType;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.Session;
+import org.b3log.latke.http.handler.Handler;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.util.Locales;
+import org.b3log.latke.util.Requests;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.repository.OptionRepository;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Locale;
+
+/**
+ * Before request handler.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Nov 11, 2019
+ * @since 3.6.0
+ */
+public class BeforeRequestHandler implements Handler {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(BeforeRequestHandler.class);
+
+ @Override
+ public void handle(final RequestContext context) {
+ Stopwatchs.start("Request initialized [" + context.requestURI() + "]");
+
+ Locales.setLocale(Latkes.getLocale());
+
+ Sessions.setTemplateDir(Symphonys.SKIN_DIR_NAME);
+ Sessions.setMobile(false);
+ Sessions.setAvatarViewMode(UserExt.USER_AVATAR_VIEW_MODE_C_ORIGINAL);
+
+ fillBotAttrs(context);
+ resolveSkinDir(context);
+ }
+
+ /**
+ * Resolve skin (template) for the specified HTTP request.
+ *
+ * @param context the specified HTTP request context
+ */
+ private void resolveSkinDir(final RequestContext context) {
+ if (Sessions.isBot() || context.getRequest().isStaticResource()) {
+ return;
+ }
+
+ Stopwatchs.start("Resolve skin");
+
+ final String templateDirName = Sessions.isMobile() ? "mobile" : "classic";
+ Sessions.setTemplateDir(templateDirName);
+
+ final Request request = context.getRequest();
+ final Session httpSession = request.getSession();
+ httpSession.setAttribute(Keys.TEMAPLTE_DIR_NAME, templateDirName);
+
+ try {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final UserQueryService userQueryService = beanManager.getReference(UserQueryService.class);
+ final OptionRepository optionRepository = beanManager.getReference(OptionRepository.class);
+
+ final JSONObject optionLang = optionRepository.get(Option.ID_C_MISC_LANGUAGE);
+ final String optionLangValue = optionLang.optString(Option.OPTION_VALUE);
+ if ("0".equals(optionLangValue)) {
+ Locales.setLocale(Locales.getLocale(request));
+ } else {
+ Locales.setLocale(Locales.getLocale(optionLangValue));
+ }
+
+ JSONObject user = userQueryService.getCurrentUser(request);
+ if (null == user) {
+ return;
+ }
+
+ final String skin = Sessions.isMobile() ? user.optString(UserExt.USER_MOBILE_SKIN) : user.optString(UserExt.USER_SKIN);
+ httpSession.setAttribute(Keys.TEMAPLTE_DIR_NAME, skin);
+ Sessions.setTemplateDir(skin);
+ Sessions.setAvatarViewMode(user.optInt(UserExt.USER_AVATAR_VIEW_MODE));
+ Sessions.setUser(user);
+ Sessions.setLoggedIn(true);
+
+ final Locale locale = Locales.getLocale(user.optString(UserExt.USER_LANGUAGE));
+ Locales.setLocale(locale);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Resolves skin failed", e);
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+
+ private static void fillBotAttrs(final RequestContext context) {
+ final String userAgentStr = context.header(Common.USER_AGENT);
+ final UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
+ BrowserType browserType = userAgent.getBrowser().getBrowserType();
+ if (StringUtils.containsIgnoreCase(userAgentStr, "mobile")
+ || StringUtils.containsIgnoreCase(userAgentStr, "MQQBrowser")
+ || StringUtils.containsIgnoreCase(userAgentStr, "iphone")
+ || StringUtils.containsIgnoreCase(userAgentStr, "MicroMessenger")
+ || StringUtils.containsIgnoreCase(userAgentStr, "CFNetwork")
+ || StringUtils.containsIgnoreCase(userAgentStr, "Android")) {
+ browserType = BrowserType.MOBILE_BROWSER;
+ } else if (StringUtils.containsIgnoreCase(userAgentStr, "Iframely")
+ || StringUtils.containsIgnoreCase(userAgentStr, "Google")
+ || StringUtils.containsIgnoreCase(userAgentStr, "BUbiNG")
+ || StringUtils.containsIgnoreCase(userAgentStr, "ltx71")) {
+ browserType = BrowserType.ROBOT;
+ } else if (BrowserType.UNKNOWN == browserType) {
+ if (!StringUtils.containsIgnoreCase(userAgentStr, "Java")
+ && !StringUtils.containsIgnoreCase(userAgentStr, "MetaURI")
+ && !StringUtils.containsIgnoreCase(userAgentStr, "Feed")
+ && !StringUtils.containsIgnoreCase(userAgentStr, "okhttp")
+ && !StringUtils.containsIgnoreCase(userAgentStr, "Sym")) {
+ LOGGER.log(Level.WARN, "Unknown client [UA=" + userAgentStr + ", remoteAddr="
+ + Requests.getRemoteAddr(context.getRequest()) + ", URI=" + context.requestURI() + "]");
+ }
+ }
+
+ if (BrowserType.ROBOT == browserType) {
+ LOGGER.log(Level.DEBUG, "Request made from a search engine [User-Agent={0}]", context.header(Common.USER_AGENT));
+ Sessions.setBot(true);
+
+ return;
+ }
+
+ Sessions.setBot(false);
+ Sessions.setMobile(BrowserType.MOBILE_BROWSER == browserType);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/BreezemoonProcessor.java b/src/main/java/org/b3log/symphony/processor/BreezemoonProcessor.java
new file mode 100644
index 000000000..bf6fe2b0c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/BreezemoonProcessor.java
@@ -0,0 +1,302 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.latke.util.Paginator;
+import org.b3log.latke.util.Requests;
+import org.b3log.symphony.model.Breezemoon;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.*;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.BreezemoonMgmtService;
+import org.b3log.symphony.service.BreezemoonQueryService;
+import org.b3log.symphony.service.DataModelService;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Breezemoon processor. https://github.com/b3log/symphony/issues/507
+ *
+ *
+ * Shows watch breezemoons (/watch/breezemoons), GET
+ * Adds a breezemoon (/breezemoon), POST
+ * Updates a breezemoon (/breezemoon/{id}), PUT
+ * Removes a breezemoon (/breezemoon/{id}), DELETE
+ * Shows a breezemoon (/breezemoon/{id}), GET
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.1.2, Jan 5, 2019
+ * @since 2.8.0
+ */
+@RequestProcessor
+public class BreezemoonProcessor {
+
+ /**
+ * Breezemoon query service.
+ */
+ @Inject
+ private BreezemoonQueryService breezemoonQueryService;
+
+ /**
+ * Breezemoon management service.
+ */
+ @Inject
+ private BreezemoonMgmtService breezemoonMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Optiona query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Shows breezemoon page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/watch/breezemoons", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showWatchBreezemoon(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "breezemoon.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ String currentUserId = null;
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+
+ currentUserId = user.optString(Keys.OBJECT_ID);
+ }
+
+ final int windowSize = Symphonys.ARTICLE_LIST_WIN_SIZE;
+ final JSONObject result = breezemoonQueryService.getFollowingUserBreezemoons(currentUserId, pageNum, pageSize, windowSize);
+ final List bms = (List) result.opt(Breezemoon.BREEZEMOONS);
+ dataModel.put(Common.WATCHING_BREEZEMOONS, bms);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put(Common.SELECTED, Common.WATCH);
+ dataModel.put(Common.CURRENT, StringUtils.substringAfter(context.requestURI(), "/watch"));
+ }
+
+ /**
+ * Adds a breezemoon.
+ *
+ * The request json object (breezemoon):
+ *
+ * {
+ * "breezemoonContent": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/breezemoon", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void addBreezemoon(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ if (isInvalid(context, requestJSONObject)) {
+ return;
+ }
+
+ final JSONObject breezemoon = new JSONObject();
+ final String breezemoonContent = requestJSONObject.optString(Breezemoon.BREEZEMOON_CONTENT);
+ breezemoon.put(Breezemoon.BREEZEMOON_CONTENT, breezemoonContent);
+ final JSONObject user = Sessions.getUser();
+ final String authorId = user.optString(Keys.OBJECT_ID);
+ breezemoon.put(Breezemoon.BREEZEMOON_AUTHOR_ID, authorId);
+ final String ip = Requests.getRemoteAddr(request);
+ breezemoon.put(Breezemoon.BREEZEMOON_IP, ip);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+ breezemoon.put(Breezemoon.BREEZEMOON_UA, ua);
+ final JSONObject address = Geos.getAddress(ip);
+ if (null != address) {
+ breezemoon.put(Breezemoon.BREEZEMOON_CITY, address.optString(Common.CITY));
+ }
+
+ try {
+ breezemoonMgmtService.addBreezemoon(breezemoon);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ } catch (final Exception e) {
+ context.renderMsg(e.getMessage());
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ /**
+ * Updates a breezemoon.
+ *
+ * The request json object (breezemoon):
+ *
+ * {
+ * "breezemoonContent": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/breezemoon/{id}", method = HttpMethod.PUT)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void updateBreezemoon(final RequestContext context) {
+ final String id = context.pathVar("id");
+ context.renderJSON();
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ if (isInvalid(context, requestJSONObject)) {
+ return;
+ }
+
+ try {
+ final JSONObject old = breezemoonQueryService.getBreezemoon(id);
+ if (null == old) {
+ throw new ServiceException(langPropsService.get("queryFailedLabel"));
+ }
+
+ final JSONObject user = Sessions.getUser();
+ if (!old.optString(Breezemoon.BREEZEMOON_AUTHOR_ID).equals(user.optString(Keys.OBJECT_ID))) {
+ throw new ServiceException(langPropsService.get("sc403Label"));
+ }
+
+ final JSONObject breezemoon = new JSONObject();
+ breezemoon.put(Keys.OBJECT_ID, id);
+ final String breezemoonContent = requestJSONObject.optString(Breezemoon.BREEZEMOON_CONTENT);
+ breezemoon.put(Breezemoon.BREEZEMOON_CONTENT, breezemoonContent);
+ final String ip = Requests.getRemoteAddr(request);
+ breezemoon.put(Breezemoon.BREEZEMOON_IP, ip);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+ breezemoon.put(Breezemoon.BREEZEMOON_UA, ua);
+
+ breezemoonMgmtService.updateBreezemoon(breezemoon);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ } catch (final Exception e) {
+ context.renderMsg(e.getMessage());
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ /**
+ * Removes a breezemoon.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/breezemoon/{id}", method = HttpMethod.DELETE)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, CSRFCheck.class, PermissionCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeBreezemoon(final RequestContext context) {
+ final String id = context.pathVar("id");
+ context.renderJSON();
+
+ try {
+ final JSONObject breezemoon = breezemoonQueryService.getBreezemoon(id);
+ if (null == breezemoon) {
+ throw new ServiceException(langPropsService.get("queryFailedLabel"));
+ }
+
+ final JSONObject user = Sessions.getUser();
+ if (!breezemoon.optString(Breezemoon.BREEZEMOON_AUTHOR_ID).equals(user.optString(Keys.OBJECT_ID))) {
+ throw new ServiceException(langPropsService.get("sc403Label"));
+ }
+
+ breezemoonMgmtService.removeBreezemoon(id);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ } catch (final Exception e) {
+ context.renderMsg(e.getMessage());
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ private boolean isInvalid(final RequestContext context, final JSONObject requestJSONObject) {
+ String breezemoonContent = requestJSONObject.optString(Breezemoon.BREEZEMOON_CONTENT);
+ breezemoonContent = StringUtils.trim(breezemoonContent);
+ final long length = StringUtils.length(breezemoonContent);
+ if (1 > length || 512 < length) {
+ context.renderMsg(langPropsService.get("breezemoonLengthLabel"));
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ return true;
+ }
+
+ if (optionQueryService.containReservedWord(breezemoonContent)) {
+ context.renderMsg(langPropsService.get("contentContainReservedWordLabel"));
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ return true;
+ }
+
+ requestJSONObject.put(Breezemoon.BREEZEMOON_CONTENT, breezemoonContent);
+
+ return false;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/CaptchaProcessor.java b/src/main/java/org/b3log/symphony/processor/CaptchaProcessor.java
new file mode 100644
index 000000000..0791fb19e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/CaptchaProcessor.java
@@ -0,0 +1,230 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.Response;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.PngRenderer;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.model.Common;
+import org.json.JSONObject;
+import org.patchca.color.GradientColorFactory;
+import org.patchca.color.RandomColorFactory;
+import org.patchca.filter.predefined.CurvesRippleFilterFactory;
+import org.patchca.font.RandomFontFactory;
+import org.patchca.service.Captcha;
+import org.patchca.service.ConfigurableCaptchaService;
+import org.patchca.word.RandomWordFactory;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Captcha processor.
+ *
+ * @author Liang Ding
+ * @version 2.3.0.6, Nov 2, 2018
+ * @since 0.2.2
+ */
+@RequestProcessor
+public class CaptchaProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(CaptchaProcessor.class);
+
+ /**
+ * Key of captcha.
+ */
+ public static final String CAPTCHA = "captcha";
+
+ /**
+ * Captchas.
+ */
+ public static final Set CAPTCHAS = new HashSet<>();
+
+ /**
+ * Captcha length.
+ */
+ private static final int CAPTCHA_LENGTH = 4;
+
+ /**
+ * Captcha chars.
+ */
+ private static final String CHARS = "acdefhijklmnprstuvwxy234578";
+
+ /**
+ * Checks whether the specified captcha is invalid.
+ *
+ * @param captcha the specified captcha
+ * @return {@code true} if it is invalid, returns {@code false} otherwise
+ */
+ public static boolean invalidCaptcha(final String captcha) {
+ if (StringUtils.isBlank(captcha) || captcha.length() != CAPTCHA_LENGTH) {
+ return true;
+ }
+
+ boolean ret = !CaptchaProcessor.CAPTCHAS.contains(captcha);
+ if (!ret) {
+ CaptchaProcessor.CAPTCHAS.remove(captcha);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Gets captcha.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/captcha", method = HttpMethod.GET)
+ public void get(final RequestContext context) {
+ final PngRenderer renderer = new PngRenderer();
+ context.setRenderer(renderer);
+
+ try {
+ final ConfigurableCaptchaService cs = new ConfigurableCaptchaService();
+ if (0.5 < Math.random()) {
+ cs.setColorFactory(new GradientColorFactory());
+ } else {
+ cs.setColorFactory(new RandomColorFactory());
+ }
+ cs.setFilterFactory(new CurvesRippleFilterFactory(cs.getColorFactory()));
+ final RandomWordFactory randomWordFactory = new RandomWordFactory();
+ randomWordFactory.setCharacters(CHARS);
+ randomWordFactory.setMinLength(CAPTCHA_LENGTH);
+ randomWordFactory.setMaxLength(CAPTCHA_LENGTH);
+ cs.setWordFactory(randomWordFactory);
+ cs.setFontFactory(new RandomFontFactory(getAvaialbeFonts()));
+
+ final Captcha captcha = cs.getCaptcha();
+ final String challenge = captcha.getChallenge();
+ final BufferedImage bufferedImage = captcha.getImage();
+
+ if (CAPTCHAS.size() > 64) {
+ CAPTCHAS.clear();
+ }
+
+ CAPTCHAS.add(challenge);
+
+ final Response response = context.getResponse();
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Expires", "0");
+
+ renderImg(renderer, bufferedImage);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Gets captcha for login.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/captcha/login", method = HttpMethod.GET)
+ public void getLoginCaptcha(final RequestContext context) {
+ try {
+ final Response response = context.getResponse();
+
+ final String userId = context.param(Common.NEED_CAPTCHA);
+ if (StringUtils.isBlank(userId)) {
+ return;
+ }
+
+ final JSONObject wrong = LoginProcessor.WRONG_PWD_TRIES.get(userId);
+ if (null == wrong) {
+ return;
+ }
+
+ if (wrong.optInt(Common.WRON_COUNT) < 3) {
+ return;
+ }
+
+ final PngRenderer renderer = new PngRenderer();
+ context.setRenderer(renderer);
+
+ final ConfigurableCaptchaService cs = new ConfigurableCaptchaService();
+ if (0.5 < Math.random()) {
+ cs.setColorFactory(new GradientColorFactory());
+ } else {
+ cs.setColorFactory(new RandomColorFactory());
+ }
+ cs.setFilterFactory(new CurvesRippleFilterFactory(cs.getColorFactory()));
+ final RandomWordFactory randomWordFactory = new RandomWordFactory();
+ randomWordFactory.setCharacters(CHARS);
+ randomWordFactory.setMinLength(CAPTCHA_LENGTH);
+ randomWordFactory.setMaxLength(CAPTCHA_LENGTH);
+ cs.setWordFactory(randomWordFactory);
+ final Captcha captcha = cs.getCaptcha();
+ final String challenge = captcha.getChallenge();
+ final BufferedImage bufferedImage = captcha.getImage();
+
+ wrong.put(CAPTCHA, challenge);
+
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Expires", "0");
+
+ renderImg(renderer, bufferedImage);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, e.getMessage(), e);
+ }
+ }
+
+ private void renderImg(final PngRenderer renderer, final BufferedImage bufferedImage) throws IOException {
+ try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ ImageIO.write(bufferedImage, "png", baos);
+ final byte[] data = baos.toByteArray();
+ renderer.setData(data);
+ }
+ }
+
+ private static List getAvaialbeFonts() {
+ final List ret = new ArrayList<>();
+
+ final GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ final Font[] fonts = e.getAllFonts();
+ for (final Font f : fonts) {
+ if (Strings.contains(f.getFontName(), new String[]{"Verdana", "DejaVu Sans Mono", "Tahoma"})) {
+ ret.add(f.getFontName());
+ }
+ }
+
+ final String defaultFontName = new JLabel().getFont().getFontName();
+ ret.add(defaultFontName);
+
+ return ret;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ChargeProcessor.java b/src/main/java/org/b3log/symphony/processor/ChargeProcessor.java
new file mode 100644
index 000000000..dbe4b6e9a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ChargeProcessor.java
@@ -0,0 +1,78 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.DataModelService;
+
+import java.util.Map;
+
+/**
+ * Charge processor.
+ *
+ * Shows point charge (/charge/point), GET
+ *
+ *
+ * @author Liang Ding
+ * @version 1.1.0.2, Oct 26, 2016
+ * @since 1.3.0
+ */
+@RequestProcessor
+public class ChargeProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ChargeProcessor.class);
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Shows charge point.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/charge/point", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showChargePoint(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "charge-point.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ChatroomProcessor.java b/src/main/java/org/b3log/symphony/processor/ChatroomProcessor.java
new file mode 100644
index 000000000..0565c10eb
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ChatroomProcessor.java
@@ -0,0 +1,213 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.util.Locales;
+import org.b3log.latke.util.Times;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.ChatMsgAddValidation;
+import org.b3log.symphony.processor.channel.ChatroomChannel;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.b3log.symphony.processor.channel.ChatroomChannel.SESSIONS;
+
+/**
+ * Chatroom processor.
+ *
+ * Shows chatroom (/cr), GET
+ * Sends chat message (/chat-room/send), POST
+ *
+ *
+ * @author Liang Ding
+ * @version 1.3.5.22, Sep 6, 2019
+ * @since 1.4.0
+ */
+@RequestProcessor
+public class ChatroomProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ChatroomProcessor.class);
+
+ /**
+ * Chat messages.
+ */
+ public static LinkedList messages = new LinkedList<>();
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * Short link query service.
+ */
+ @Inject
+ private ShortLinkQueryService shortLinkQueryService;
+
+ /**
+ * Notification query service.
+ */
+ @Inject
+ private NotificationQueryService notificationQueryService;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Comment management service.
+ */
+ @Inject
+ private CommentMgmtService commentMgmtService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Adds a chat message.
+ *
+ * The request json object (a chat message):
+ *
+ * {
+ * "content": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/chat-room/send", method = HttpMethod.POST)
+ @Before({LoginCheck.class, ChatMsgAddValidation.class})
+ public synchronized void addChatRoomMsg(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+ String content = requestJSONObject.optString(Common.CONTENT);
+
+ content = shortLinkQueryService.linkArticle(content);
+ content = Emotions.convert(content);
+ content = Markdowns.toHTML(content);
+ content = Markdowns.clean(content, "");
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userName = currentUser.optString(User.USER_NAME);
+
+ final JSONObject msg = new JSONObject();
+ msg.put(User.USER_NAME, userName);
+ msg.put(UserExt.USER_AVATAR_URL, currentUser.optString(UserExt.USER_AVATAR_URL));
+ msg.put(Common.CONTENT, content);
+ msg.put(Common.TIME, System.currentTimeMillis());
+
+ messages.addFirst(msg);
+ final int maxCnt = Symphonys.CHATROOMMSGS_CNT;
+ if (messages.size() > maxCnt) {
+ messages.remove(maxCnt);
+ }
+
+ final JSONObject pushMsg = JSONs.clone(msg);
+ pushMsg.put(Common.TIME, Times.getTimeAgo(msg.optLong(Common.TIME), Locales.getLocale()));
+ ChatroomChannel.notifyChat(pushMsg);
+
+ context.renderTrueResult();
+
+ try {
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final JSONObject user = userQueryService.getUser(userId);
+ user.put(UserExt.USER_LATEST_CMT_TIME, System.currentTimeMillis());
+ userMgmtService.updateUser(userId, user);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Update user latest comment time failed", e);
+ }
+ }
+
+ /**
+ * Shows chatroom.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/cr", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showChatRoom(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "chat-room.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final List msgs = messages.stream().
+ map(msg -> JSONs.clone(msg).put(Common.TIME, Times.getTimeAgo(msg.optLong(Common.TIME), Locales.getLocale()))).collect(Collectors.toList());
+ dataModel.put(Common.MESSAGES, msgs);
+ dataModel.put("chatRoomMsgCnt", Symphonys.CHATROOMMSGS_CNT);
+ dataModel.put(Common.ONLINE_CHAT_CNT, SESSIONS.size());
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/CityProcessor.java b/src/main/java/org/b3log/symphony/processor/CityProcessor.java
new file mode 100644
index 000000000..d6a245808
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/CityProcessor.java
@@ -0,0 +1,284 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Paginator;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * City processor.
+ *
+ * Shows city articles (/city/{city}), GET
+ * Show city users (/city/{city}/users), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Zephyr
+ * @version 1.3.1.12, Jan 5, 2019
+ * @since 1.3.0
+ */
+@RequestProcessor
+public class CityProcessor {
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Avatar query service.
+ */
+ @Inject
+ private AvatarQueryService avatarQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langService;
+
+ /**
+ * Show city articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/city/{city}", "/city/{city}/articles"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showCityArticles(final RequestContext context) {
+ final String city = context.pathVar("city");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "city.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ dataModel.put(Common.CURRENT, "");
+
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ List articles = new ArrayList<>();
+ dataModel.put(Article.ARTICLES, articles); // an empty list to avoid null check in template
+ dataModel.put(Common.SELECTED, Common.CITY);
+
+ final JSONObject user = Sessions.getUser();
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+
+ dataModel.put(UserExt.USER_GEO_STATUS, true);
+ dataModel.put(Common.CITY_FOUND, true);
+ dataModel.put(Common.CITY, langService.get("sameCityLabel"));
+
+ if (UserExt.USER_GEO_STATUS_C_PUBLIC != user.optInt(UserExt.USER_GEO_STATUS)) {
+ dataModel.put(UserExt.USER_GEO_STATUS, false);
+
+ return;
+ }
+
+ final String userCity = user.optString(UserExt.USER_CITY);
+
+ String queryCity = city;
+ if ("my".equals(city)) {
+ dataModel.put(Common.CITY, userCity);
+ queryCity = userCity;
+ } else {
+ dataModel.put(Common.CITY, city);
+ }
+
+ if (StringUtils.isBlank(userCity)) {
+ dataModel.put(Common.CITY_FOUND, false);
+
+ return;
+ }
+
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+ final int windowSize = Symphonys.ARTICLE_LIST_WIN_SIZE;
+
+ final JSONObject statistic = optionQueryService.getOption(queryCity + "-ArticleCount");
+ if (null != statistic) {
+ articles = articleQueryService.getArticlesByCity(queryCity, pageNum, pageSize);
+ dataModel.put(Article.ARTICLES, articles);
+ }
+
+ final int articleCnt = null == statistic ? 0 : statistic.optInt(Option.OPTION_VALUE);
+ final int pageCount = (int) Math.ceil(articleCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ }
+
+ /**
+ * Show city users.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/city/{city}/users", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showCityUsers(final RequestContext context) {
+ final String city = context.pathVar("city");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "city.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ dataModel.put(Common.CURRENT, "/users");
+
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ List users = new ArrayList<>();
+ dataModel.put(User.USERS, users);
+ dataModel.put(Common.SELECTED, Common.CITY);
+
+ final JSONObject user = Sessions.getUser();
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+
+ dataModel.put(UserExt.USER_GEO_STATUS, true);
+ dataModel.put(Common.CITY_FOUND, true);
+ dataModel.put(Common.CITY, langService.get("sameCityLabel"));
+ if (UserExt.USER_GEO_STATUS_C_PUBLIC != user.optInt(UserExt.USER_GEO_STATUS)) {
+ dataModel.put(UserExt.USER_GEO_STATUS, false);
+
+ return;
+ }
+
+ final String userCity = user.optString(UserExt.USER_CITY);
+
+ String queryCity = city;
+ if ("my".equals(city)) {
+ dataModel.put(Common.CITY, userCity);
+ queryCity = userCity;
+ } else {
+ dataModel.put(Common.CITY, city);
+ }
+
+ if (StringUtils.isBlank(userCity)) {
+ dataModel.put(Common.CITY_FOUND, false);
+
+ return;
+ }
+
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.CITY_USERS_CNT;
+ final int windowSize = Symphonys.CITY_USERS_WIN_SIZE;
+
+ final JSONObject requestJSONObject = new JSONObject();
+ requestJSONObject.put(Keys.OBJECT_ID, user.optString(Keys.OBJECT_ID));
+ requestJSONObject.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ requestJSONObject.put(Pagination.PAGINATION_PAGE_SIZE, pageSize);
+ requestJSONObject.put(Pagination.PAGINATION_WINDOW_SIZE, windowSize);
+ final long latestLoginTime = DateUtils.addDays(new Date(), Integer.MIN_VALUE).getTime(); // all users
+ requestJSONObject.put(UserExt.USER_LATEST_LOGIN_TIME, latestLoginTime);
+ requestJSONObject.put(UserExt.USER_CITY, queryCity);
+ final JSONObject result = userQueryService.getUsersByCity(requestJSONObject);
+ final JSONArray cityUsers = result.optJSONArray(User.USERS);
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ if (null != cityUsers && cityUsers.length() > 0) {
+ for (int i = 0; i < cityUsers.length(); i++) {
+ users.add(cityUsers.getJSONObject(i));
+ }
+ dataModel.put(User.USERS, users);
+ }
+
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/CommentProcessor.java b/src/main/java/org/b3log/symphony/processor/CommentProcessor.java
new file mode 100644
index 000000000..44bf09729
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/CommentProcessor.java
@@ -0,0 +1,539 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.latke.util.Requests;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.CSRFCheck;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionCheck;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.CommentAddValidation;
+import org.b3log.symphony.processor.advice.validate.CommentUpdateValidation;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Comment processor.
+ *
+ * Adds a comment (/comment) locally , POST
+ * Updates a comment (/comment/{id}) locally , PUT
+ * Gets a comment's content (/comment/{id}/content), GET
+ * Thanks a comment (/comment/thank), POST
+ * Gets a comment's replies (/comment/replies), GET
+ * Gets a comment's revisions (/commment/{id}/revisions), GET
+ * Removes a comment (/comment/{id}/remove), POST
+ * Accepts a comment (/comment/accept), POST
+ *
+ *
+ * @author Liang Ding
+ * @version 1.8.0.5, Dec 16, 2018
+ * @since 0.2.0
+ */
+@RequestProcessor
+public class CommentProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(CommentProcessor.class);
+
+ /**
+ * Revision query service.
+ */
+ @Inject
+ private RevisionQueryService revisionQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Comment management service.
+ */
+ @Inject
+ private CommentMgmtService commentMgmtService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Reward query service.
+ */
+ @Inject
+ private RewardQueryService rewardQueryService;
+
+ /**
+ * Short link query service.
+ */
+ @Inject
+ private ShortLinkQueryService shortLinkQueryService;
+
+ /**
+ * Follow management service.
+ */
+ @Inject
+ private FollowMgmtService followMgmtService;
+
+ /**
+ * Accepts a comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/accept", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, PermissionCheck.class})
+ public void acceptComment(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final String commentId = requestJSONObject.optString(Comment.COMMENT_T_ID);
+
+ try {
+ final JSONObject comment = commentQueryService.getComment(commentId);
+ if (null == comment) {
+ context.renderFalseResult().renderMsg("Not found comment to accept");
+
+ return;
+ }
+ final String commentAuthorId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+ if (StringUtils.equals(userId, commentAuthorId)) {
+ context.renderFalseResult().renderMsg(langPropsService.get("thankSelfLabel"));
+
+ return;
+ }
+
+ final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ if (!StringUtils.equals(userId, article.optString(Article.ARTICLE_AUTHOR_ID))) {
+ context.renderFalseResult().renderMsg(langPropsService.get("sc403Label"));
+
+ return;
+ }
+
+ commentMgmtService.acceptComment(commentId);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Removes a comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/{id}/remove", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, PermissionCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void removeComment(final RequestContext context) {
+ final String id = context.pathVar("id");
+ final Request request = context.getRequest();
+ if (StringUtils.isBlank(id)) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String currentUserId = currentUser.optString(Keys.OBJECT_ID);
+ final JSONObject comment = commentQueryService.getComment(id);
+ if (null == comment) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final String authorId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+ if (!authorId.equals(currentUserId)) {
+ context.sendError(403);
+
+ return;
+ }
+
+ context.renderJSON();
+ try {
+ commentMgmtService.removeComment(id);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ context.renderJSONValue(Comment.COMMENT_T_ID, id);
+ } catch (final ServiceException e) {
+ final String msg = e.getMessage();
+
+ context.renderMsg(msg);
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+ /**
+ * Gets a comment's revisions.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/{id}/revisions", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class, PermissionCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void getCommentRevisions(final RequestContext context) {
+ final String id = context.pathVar("id");
+ final JSONObject viewer = Sessions.getUser();
+ final List revisions = revisionQueryService.getCommentRevisions(viewer, id);
+ final JSONObject ret = new JSONObject();
+ ret.put(Keys.STATUS_CODE, true);
+ ret.put(Revision.REVISIONS, (Object) revisions);
+
+ context.renderJSON(ret);
+ }
+
+ /**
+ * Gets a comment's content.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/{id}/content", method = HttpMethod.GET)
+ @Before({LoginCheck.class})
+ public void getCommentContent(final RequestContext context) {
+ final String id = context.pathVar("id");
+ context.renderJSON().renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ final JSONObject comment = commentQueryService.getComment(id);
+ if (null == comment) {
+ LOGGER.warn("Not found comment [id=" + id + "] to update");
+
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (!currentUser.optString(Keys.OBJECT_ID).equals(comment.optString(Comment.COMMENT_AUTHOR_ID))) {
+ context.sendError(403);
+
+ return;
+ }
+
+ context.renderJSONValue(Comment.COMMENT_CONTENT, comment.optString(Comment.COMMENT_CONTENT));
+ context.renderJSONValue(Comment.COMMENT_VISIBLE, comment.optInt(Comment.COMMENT_VISIBLE));
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ }
+
+ /**
+ * Updates a comment locally.
+ *
+ * The request json object:
+ *
+ * {
+ * "commentContent": "",
+ * "commentVisible": boolean
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/{id}", method = HttpMethod.PUT)
+ @Before({CSRFCheck.class, LoginCheck.class, CommentUpdateValidation.class, PermissionCheck.class})
+ public void updateComment(final RequestContext context) {
+ final String id = context.pathVar("id");
+ context.renderJSON().renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ final Request request = context.getRequest();
+
+ try {
+ final JSONObject comment = commentQueryService.getComment(id);
+ if (null == comment) {
+ LOGGER.warn("Not found comment [id=" + id + "] to update");
+
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (!currentUser.optString(Keys.OBJECT_ID).equals(comment.optString(Comment.COMMENT_AUTHOR_ID))) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+
+ String commentContent = requestJSONObject.optString(Comment.COMMENT_CONTENT);
+ final boolean isOnlyAuthorVisible = requestJSONObject.optBoolean(Comment.COMMENT_VISIBLE);
+ final String ip = Requests.getRemoteAddr(request);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+
+ comment.put(Comment.COMMENT_CONTENT, commentContent);
+ comment.put(Comment.COMMENT_IP, "");
+ if (StringUtils.isNotBlank(ip)) {
+ comment.put(Comment.COMMENT_IP, ip);
+ }
+ comment.put(Comment.COMMENT_UA, "");
+ if (StringUtils.isNotBlank(ua)) {
+ comment.put(Comment.COMMENT_UA, ua);
+ }
+ comment.put(Comment.COMMENT_VISIBLE, isOnlyAuthorVisible
+ ? Comment.COMMENT_VISIBLE_C_AUTHOR : Comment.COMMENT_VISIBLE_C_ALL);
+
+ commentMgmtService.updateComment(comment.optString(Keys.OBJECT_ID), comment);
+
+ commentContent = comment.optString(Comment.COMMENT_CONTENT);
+ commentContent = shortLinkQueryService.linkArticle(commentContent);
+ commentContent = Emotions.toAliases(commentContent);
+ commentContent = Emotions.convert(commentContent);
+ commentContent = Markdowns.toHTML(commentContent);
+ commentContent = Markdowns.clean(commentContent, "");
+ commentContent = MP3Players.render(commentContent);
+ commentContent = VideoPlayers.render(commentContent);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ context.renderJSONValue(Comment.COMMENT_CONTENT, commentContent);
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Gets a comment's original comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/original", method = HttpMethod.POST)
+ public void getOriginalComment(final RequestContext context) {
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String commentId = requestJSONObject.optString(Comment.COMMENT_T_ID);
+ int commentViewMode = requestJSONObject.optInt(UserExt.USER_COMMENT_VIEW_MODE);
+ final JSONObject currentUser = Sessions.getUser();
+ String currentUserId = null;
+ if (null != currentUser) {
+ currentUserId = currentUser.optString(Keys.OBJECT_ID);
+ }
+
+ final JSONObject originalCmt = commentQueryService.getOriginalComment(currentUserId, commentViewMode, commentId);
+
+ // Fill thank
+ final String originalCmtId = originalCmt.optString(Keys.OBJECT_ID);
+
+ if (null != currentUser) {
+ originalCmt.put(Common.REWARDED,
+ rewardQueryService.isRewarded(currentUser.optString(Keys.OBJECT_ID),
+ originalCmtId, Reward.TYPE_C_COMMENT));
+ }
+
+ context.renderJSON(true).renderJSONValue(Comment.COMMENT_T_REPLIES, originalCmt);
+ }
+
+ /**
+ * Gets a comment's replies.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/replies", method = HttpMethod.POST)
+ public void getReplies(final RequestContext context) {
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String commentId = requestJSONObject.optString(Comment.COMMENT_T_ID);
+ int commentViewMode = requestJSONObject.optInt(UserExt.USER_COMMENT_VIEW_MODE);
+ final JSONObject currentUser = Sessions.getUser();
+ String currentUserId = null;
+ if (null != currentUser) {
+ currentUserId = currentUser.optString(Keys.OBJECT_ID);
+ }
+
+ if (StringUtils.isBlank(commentId)) {
+ context.renderJSON(true).renderJSONValue(Comment.COMMENT_T_REPLIES, Collections.emptyList());
+
+ return;
+ }
+
+ final List replies = commentQueryService.getReplies(currentUserId, commentViewMode, commentId);
+
+ // Fill reply thank
+ for (final JSONObject reply : replies) {
+ final String replyId = reply.optString(Keys.OBJECT_ID);
+
+ if (null != currentUser) {
+ reply.put(Common.REWARDED,
+ rewardQueryService.isRewarded(currentUser.optString(Keys.OBJECT_ID),
+ replyId, Reward.TYPE_C_COMMENT));
+ }
+
+ final int rewardCount = reply.optInt(Comment.COMMENT_THANK_CNT);
+ reply.put(Common.REWARED_COUNT, rewardCount);
+ }
+
+ context.renderJSON(true).renderJSONValue(Comment.COMMENT_T_REPLIES, replies);
+ }
+
+ /**
+ * Adds a comment locally.
+ *
+ * The request json object (a comment):
+ *
+ * {
+ * "articleId": "",
+ * "commentContent": "",
+ * "commentAnonymous": boolean,
+ * "commentVisible": boolean,
+ * "commentOriginalCommentId": "", // optional
+ * "userCommentViewMode": int
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment", method = HttpMethod.POST)
+ @Before({CSRFCheck.class, LoginCheck.class, CommentAddValidation.class, PermissionCheck.class})
+ public void addComment(final RequestContext context) {
+ context.renderJSON().renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+
+ final String articleId = requestJSONObject.optString(Article.ARTICLE_T_ID);
+ final String commentContent = requestJSONObject.optString(Comment.COMMENT_CONTENT);
+ final String commentOriginalCommentId = requestJSONObject.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID);
+ final int commentViewMode = requestJSONObject.optInt(UserExt.USER_COMMENT_VIEW_MODE);
+ final String ip = Requests.getRemoteAddr(request);
+ final String ua = Headers.getHeader(request, Common.USER_AGENT, "");
+
+ final boolean isAnonymous = requestJSONObject.optBoolean(Comment.COMMENT_ANONYMOUS);
+ final boolean isOnlyAuthorVisible = requestJSONObject.optBoolean(Comment.COMMENT_VISIBLE);
+
+ final JSONObject comment = new JSONObject();
+ comment.put(Comment.COMMENT_CONTENT, commentContent);
+ comment.put(Comment.COMMENT_ON_ARTICLE_ID, articleId);
+ comment.put(UserExt.USER_COMMENT_VIEW_MODE, commentViewMode);
+ comment.put(Comment.COMMENT_IP, "");
+ if (StringUtils.isNotBlank(ip)) {
+ comment.put(Comment.COMMENT_IP, ip);
+ }
+ comment.put(Comment.COMMENT_UA, "");
+ if (StringUtils.isNotBlank(ua)) {
+ comment.put(Comment.COMMENT_UA, ua);
+ }
+ comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, commentOriginalCommentId);
+
+ try {
+ final JSONObject currentUser = Sessions.getUser();
+ final String currentUserName = currentUser.optString(User.USER_NAME);
+ final JSONObject article = articleQueryService.getArticle(articleId);
+ final String articleContent = article.optString(Article.ARTICLE_CONTENT);
+ final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+ final JSONObject articleAuthor = userQueryService.getUser(articleAuthorId);
+ final String articleAuthorName = articleAuthor.optString(User.USER_NAME);
+
+ final Set userNames = userQueryService.getUserNames(articleContent);
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)
+ && !articleAuthorName.equals(currentUserName)) {
+ boolean invited = false;
+ for (final String userName : userNames) {
+ if (userName.equals(currentUserName)) {
+ invited = true;
+
+ break;
+ }
+ }
+
+ if (!invited) {
+ context.sendError(403);
+
+ return;
+ }
+ }
+
+ final String commentAuthorId = currentUser.optString(Keys.OBJECT_ID);
+ comment.put(Comment.COMMENT_AUTHOR_ID, commentAuthorId);
+ comment.put(Comment.COMMENT_ANONYMOUS, isAnonymous
+ ? Comment.COMMENT_ANONYMOUS_C_ANONYMOUS : Comment.COMMENT_ANONYMOUS_C_PUBLIC);
+ comment.put(Comment.COMMENT_VISIBLE, isOnlyAuthorVisible
+ ? Comment.COMMENT_VISIBLE_C_AUTHOR : Comment.COMMENT_VISIBLE_C_ALL);
+
+ commentMgmtService.addComment(comment);
+
+ if ((!commentAuthorId.equals(articleAuthorId) &&
+ UserExt.USER_XXX_STATUS_C_ENABLED == currentUser.optInt(UserExt.USER_REPLY_WATCH_ARTICLE_STATUS))
+ || Article.ARTICLE_TYPE_C_DISCUSSION == article.optInt(Article.ARTICLE_TYPE)) {
+ followMgmtService.watchArticle(commentAuthorId, articleId);
+ }
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Thanks a comment.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/comment/thank", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, PermissionCheck.class})
+ public void thankComment(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final JSONObject currentUser = Sessions.getUser();
+ final String commentId = requestJSONObject.optString(Comment.COMMENT_T_ID);
+
+ try {
+ commentMgmtService.thankComment(commentId, currentUser.optString(Keys.OBJECT_ID));
+
+ context.renderTrueResult().renderMsg(langPropsService.get("thankSentLabel"));
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/DomainProcessor.java b/src/main/java/org/b3log/symphony/processor/DomainProcessor.java
new file mode 100644
index 000000000..fcfff816d
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/DomainProcessor.java
@@ -0,0 +1,181 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.util.Paginator;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Domain processor.
+ *
+ * Shows domains (/domains), GET
+ * Shows domain article (/domain/{domainURI}), GET
+ *
+ *
+ * @author Liang Ding
+ * @version 1.1.0.12, Jan 5, 2019
+ * @since 1.4.0
+ */
+@RequestProcessor
+public class DomainProcessor {
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Domain query service.
+ */
+ @Inject
+ private DomainQueryService domainQueryService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Shows domain articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/domain/{domainURI}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showDomainArticles(final RequestContext context) {
+ final String domainURI = context.pathVar("domainURI");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "domain-articles.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+ }
+
+ final JSONObject domain = domainQueryService.getByURI(domainURI);
+ if (null == domain) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final List tags = domainQueryService.getTags(domain.optString(Keys.OBJECT_ID));
+ domain.put(Domain.DOMAIN_T_TAGS, (Object) tags);
+
+ dataModel.put(Domain.DOMAIN, domain);
+ dataModel.put(Common.SELECTED, domain.optString(Domain.DOMAIN_URI));
+
+ final String domainId = domain.optString(Keys.OBJECT_ID);
+
+ final JSONObject result = articleQueryService.getDomainArticles(domainId, pageNum, pageSize);
+ final List latestArticles = (List) result.opt(Article.ARTICLES);
+ dataModel.put(Common.LATEST_ARTICLES, latestArticles);
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+
+ final List pageNums = (List) pagination.opt(Pagination.PAGINATION_PAGE_NUMS);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Shows domains.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/domains", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showDomains(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "domains.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject statistic = optionQueryService.getStatistic();
+ final int tagCnt = statistic.optInt(Option.ID_C_STATISTIC_TAG_COUNT);
+ dataModel.put(Tag.TAG_T_COUNT, tagCnt);
+
+ final int domainCnt = statistic.optInt(Option.ID_C_STATISTIC_DOMAIN_COUNT);
+ dataModel.put(Domain.DOMAIN_T_COUNT, domainCnt);
+
+ final List domains = domainQueryService.getAllDomains();
+ dataModel.put(Common.ALL_DOMAINS, domains);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ErrorProcessor.java b/src/main/java/org/b3log/symphony/processor/ErrorProcessor.java
new file mode 100644
index 000000000..b1b1b306f
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ErrorProcessor.java
@@ -0,0 +1,110 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Locales;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Permission;
+import org.b3log.symphony.model.Role;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.DataModelService;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+import java.util.Map;
+
+/**
+ * Error processor.
+ *
+ * @author Liang Ding
+ * @version 1.2.1.0, Mar 30, 2019
+ * @since 0.2.0
+ */
+@Singleton
+public class ErrorProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ErrorProcessor.class);
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ /**
+ * Handles the error.
+ *
+ * @param context the specified context
+ */
+ @Before(StopwatchStartAdvice.class)
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void handleErrorPage(final RequestContext context) {
+ final String statusCode = context.pathVar("statusCode");
+ if (StringUtils.equals("GET", context.method())) {
+ final String requestURI = context.requestURI();
+ final String templateName = statusCode + ".ftl";
+ LOGGER.log(Level.TRACE, "Shows error page[requestURI={0}, templateName={1}]", requestURI, templateName);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "error/" + templateName);
+ final Map dataModel = renderer.getDataModel();
+ dataModel.putAll(langPropsService.getAll(Locales.getLocale()));
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+
+ final JSONObject user = Sessions.getUser();
+ final String roleId = null != user ? user.optString(User.USER_ROLE) : Role.ROLE_ID_C_VISITOR;
+ final Map permissionsGrant = roleQueryService.getPermissionsGrantMap(roleId);
+ dataModel.put(Permission.PERMISSIONS, permissionsGrant);
+
+ dataModel.put(Common.ELAPSED, 0);
+ } else {
+ context.renderJSON().renderMsg(statusCode);
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/FeedProcessor.java b/src/main/java/org/b3log/symphony/processor/FeedProcessor.java
new file mode 100644
index 000000000..4070305b1
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/FeedProcessor.java
@@ -0,0 +1,210 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.RssRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Locales;
+import org.b3log.symphony.Server;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.model.feed.RSSCategory;
+import org.b3log.symphony.model.feed.RSSChannel;
+import org.b3log.symphony.model.feed.RSSItem;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.service.DomainQueryService;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.service.ShortLinkQueryService;
+import org.b3log.symphony.util.Emotions;
+import org.b3log.symphony.util.Markdowns;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Feed RSS processor.
+ *
+ *
+ * Generates recent articles' RSS (/rss/recent.xml), GET/HEAD
+ * Generates domain articles' RSS (/rss/domain/{domainURL}.xml), GET/HEAD
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Aug 20, 2018
+ * @since 3.1.0
+ */
+@RequestProcessor
+public class FeedProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(FeedProcessor.class);
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Domain query service.
+ */
+ @Inject
+ private DomainQueryService domainQueryService;
+
+ /**
+ * Short link query service.
+ */
+ @Inject
+ private ShortLinkQueryService shortLinkQueryService;
+
+ /**
+ * Generates recent articles' RSS.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/rss/recent.xml", method = {HttpMethod.GET, HttpMethod.HEAD})
+ public void genRecentRSS(final RequestContext context) {
+ final RssRenderer renderer = new RssRenderer();
+ context.setRenderer(renderer);
+
+ try {
+ final RSSChannel channel = new RSSChannel();
+ final JSONObject result = articleQueryService.getRecentArticles(0, 1, Symphonys.ARTICLE_LIST_CNT);
+ final List articles = (List) result.get(Article.ARTICLES);
+ for (int i = 0; i < articles.size(); i++) {
+ RSSItem item = getItem(articles, i);
+ channel.addItem(item);
+ }
+ channel.setTitle(langPropsService.get("symphonyLabel"));
+ channel.setLastBuildDate(new Date());
+ channel.setLink(Latkes.getServePath());
+ channel.setAtomLink(Latkes.getServePath() + "/rss/recent.xml");
+ channel.setGenerator("Symphony v" + Server.VERSION + ", https://sym.b3log.org");
+ final String localeString = optionQueryService.getOption("miscLanguage").optString(Option.OPTION_VALUE);
+ final String country = Locales.getCountry(localeString).toLowerCase();
+ final String language = Locales.getLanguage(localeString).toLowerCase();
+ channel.setLanguage(language + '-' + country);
+ channel.setDescription(langPropsService.get("symDescriptionLabel"));
+
+ renderer.setContent(channel.toString());
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Generates recent articles' RSS failed", e);
+ context.getResponse().sendError(500);
+ }
+ }
+
+ /**
+ * Generates domain articles' RSS.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/rss/domain/{domainURI}.xml", method = {HttpMethod.GET, HttpMethod.HEAD})
+ public void genDomainRSS(final RequestContext context) {
+ final String domainURI = context.pathVar("domainURI");
+ final RssRenderer renderer = new RssRenderer();
+ context.setRenderer(renderer);
+
+ try {
+ final JSONObject domain = domainQueryService.getByURI(domainURI);
+ if (null == domain) {
+ context.getResponse().sendError(404);
+
+ return;
+ }
+
+ final RSSChannel channel = new RSSChannel();
+ final String domainId = domain.optString(Keys.OBJECT_ID);
+ final JSONObject result = articleQueryService.getDomainArticles(domainId, 1, Symphonys.ARTICLE_LIST_CNT);
+ final List articles = (List) result.get(Article.ARTICLES);
+ for (int i = 0; i < articles.size(); i++) {
+ RSSItem item = getItem(articles, i);
+ channel.addItem(item);
+ }
+ channel.setTitle(langPropsService.get("symphonyLabel"));
+ channel.setLastBuildDate(new Date());
+ channel.setLink(Latkes.getServePath());
+ channel.setAtomLink(Latkes.getServePath() + "/rss/" + domainURI + ".xml");
+ channel.setGenerator("Symphony v" + Server.VERSION + ", https://sym.b3log.org");
+ final String localeString = optionQueryService.getOption("miscLanguage").optString(Option.OPTION_VALUE);
+ final String country = Locales.getCountry(localeString).toLowerCase();
+ final String language = Locales.getLanguage(localeString).toLowerCase();
+ channel.setLanguage(language + '-' + country);
+ channel.setDescription(langPropsService.get("symDescriptionLabel"));
+
+ renderer.setContent(channel.toString());
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Generates recent articles' RSS failed", e);
+
+ context.getResponse().sendError(500);
+ }
+ }
+
+ private RSSItem getItem(final List articles, int i) throws org.json.JSONException {
+ final JSONObject article = articles.get(i);
+ final RSSItem ret = new RSSItem();
+ String title = article.getString(Article.ARTICLE_TITLE);
+ title = Emotions.toAliases(title);
+ ret.setTitle(title);
+ String description = article.getString(Article.ARTICLE_CONTENT);
+ description = shortLinkQueryService.linkArticle(description);
+ description = Emotions.toAliases(description);
+ description = Emotions.convert(description);
+ description = Markdowns.toHTML(description);
+ ret.setDescription(description);
+ final Date pubDate = (Date) article.get(Article.ARTICLE_UPDATE_TIME);
+ ret.setPubDate(pubDate);
+ final String link = Latkes.getServePath() + article.getString(Article.ARTICLE_PERMALINK);
+ ret.setLink(link);
+ ret.setGUID(link);
+ ret.setAuthor(article.optString(Article.ARTICLE_T_AUTHOR_NAME));
+ final String tagsString = article.getString(Article.ARTICLE_TAGS);
+ final String[] tagStrings = tagsString.split(",");
+ for (final String tagString : tagStrings) {
+ final RSSCategory catetory = new RSSCategory();
+ ret.addCatetory(catetory);
+ catetory.setTerm(tagString);
+ }
+
+ return ret;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/FetchUploadProcessor.java b/src/main/java/org/b3log/symphony/processor/FetchUploadProcessor.java
new file mode 100644
index 000000000..661d43fc7
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/FetchUploadProcessor.java
@@ -0,0 +1,174 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import com.qiniu.storage.Configuration;
+import com.qiniu.storage.UploadManager;
+import com.qiniu.util.Auth;
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.UUID;
+
+/**
+ * Fetch file and upload processor.
+ *
+ *
+ * Fetches the remote file and upload it (/fetch-upload), POST
+ *
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.2.0, May 8, 2019
+ * @since 1.5.0
+ */
+@RequestProcessor
+public class FetchUploadProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(FetchUploadProcessor.class);
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Fetches the remote file and upload it.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/fetch-upload", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void fetchUpload(final RequestContext context) {
+ final JSONObject result = Results.newFail();
+ context.renderJSONPretty(result);
+ final JSONObject data = new JSONObject();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String originalURL = requestJSONObject.optString(Common.URL);
+ if (!Strings.isURL(originalURL) || !StringUtils.startsWithIgnoreCase(originalURL, "http")) {
+ return;
+ }
+
+ byte[] bytes;
+ String contentType;
+ try {
+ final String host = new URL(originalURL).getHost();
+ final String hostIp = InetAddress.getByName(host).getHostAddress();
+ if (Networks.isInnerAddress(hostIp)) {
+ return;
+ }
+
+ final HttpRequest req = HttpRequest.get(originalURL).header(Common.USER_AGENT, Symphonys.USER_AGENT_BOT);
+ final HttpResponse res = req.connectionTimeout(3000).timeout(5000).send();
+ res.close();
+ if (200 != res.statusCode()) {
+ return;
+ }
+
+ bytes = res.bodyBytes();
+ contentType = res.contentType();
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Fetch file [url=" + originalURL + "] failed", e);
+
+ return;
+ }
+
+ final String suffix = Headers.getSuffix(contentType);
+ final String[] allowedSuffixArray = Symphonys.UPLOAD_SUFFIX.split(",");
+ if (!Strings.containsIgnoreCase(suffix, allowedSuffixArray)) {
+ String msg = langPropsService.get("invalidFileSuffixLabel");
+ msg = StringUtils.replace(msg, "${suffix}", suffix);
+ result.put(Keys.MSG, msg);
+
+ return;
+ }
+
+ String fileName = UUID.randomUUID().toString().replace("-", "") + "." + suffix;
+
+ if (Symphonys.QN_ENABLED) {
+ final Auth auth = Auth.create(Symphonys.UPLOAD_QINIU_AK, Symphonys.UPLOAD_QINIU_SK);
+ final UploadManager uploadManager = new UploadManager(new Configuration());
+
+ try {
+ uploadManager.put(bytes, "e/" + fileName, auth.uploadToken(Symphonys.UPLOAD_QINIU_BUCKET),
+ null, contentType, false);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Uploads to Qiniu failed", e);
+ }
+
+ data.put(Common.URL, Symphonys.UPLOAD_QINIU_DOMAIN + "/e/" + fileName);
+ data.put("originalURL", originalURL);
+ } else {
+ fileName = FileUploadProcessor.genFilePath(fileName);
+ final Path path = Paths.get(Symphonys.UPLOAD_LOCAL_DIR, fileName);
+ path.getParent().toFile().mkdirs();
+ try (final OutputStream output = new FileOutputStream(Symphonys.UPLOAD_LOCAL_DIR + fileName)) {
+ IOUtils.write(bytes, output);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Writes output stream failed", e);
+ }
+
+ data.put(Common.URL, Latkes.getServePath() + "/upload/" + fileName);
+ data.put("originalURL", originalURL);
+ }
+
+ result.put(Common.DATA, data);
+ result.put(Keys.CODE, StatusCodes.SUCC);
+ result.put(Keys.MSG, "");
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/FileUploadProcessor.java b/src/main/java/org/b3log/symphony/processor/FileUploadProcessor.java
new file mode 100644
index 000000000..882a2e799
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/FileUploadProcessor.java
@@ -0,0 +1,270 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import com.qiniu.storage.Configuration;
+import com.qiniu.storage.UploadManager;
+import com.qiniu.util.Auth;
+import jodd.io.FileUtil;
+import jodd.net.MimeTypes;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.*;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Strings;
+import org.b3log.latke.util.URLs;
+import org.b3log.symphony.Server;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.b3log.symphony.util.Symphonys.QN_ENABLED;
+
+/**
+ * File upload to local.
+ *
+ * @author Liang Ding
+ * @author Liyuan Li
+ * @version 2.0.1.8, Nov 5, 2019
+ * @since 1.4.0
+ */
+@RequestProcessor
+public class FileUploadProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(FileUploadProcessor.class);
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Gets file by the specified URL.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/upload/{yyyy}/{MM}/{file}", method = HttpMethod.GET)
+ public void getFile(final RequestContext context) {
+ if (QN_ENABLED) {
+ return;
+ }
+
+ final Response response = context.getResponse();
+
+ final String uri = context.requestURI();
+ String key = StringUtils.substringAfter(uri, "/upload/");
+ key = StringUtils.substringBeforeLast(key, "?"); // Erase Qiniu template
+ key = StringUtils.substringBeforeLast(key, "?"); // Erase Qiniu template
+
+ String path = Symphonys.UPLOAD_LOCAL_DIR + key;
+ path = URLs.decode(path);
+
+ try {
+ if (!FileUtil.isExistingFile(new File(path)) ||
+ !FileUtil.isExistingFolder(new File(Symphonys.UPLOAD_LOCAL_DIR)) ||
+ !new File(path).getCanonicalPath().startsWith(new File(Symphonys.UPLOAD_LOCAL_DIR).getCanonicalPath())) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final byte[] data = IOUtils.toByteArray(new FileInputStream(path));
+
+ final String ifNoneMatch = context.header("If-None-Match");
+ final String etag = "\"" + DigestUtils.md5Hex(new String(data)) + "\"";
+
+ context.setHeader("Cache-Control", "public, max-age=31536000");
+ context.setHeader("ETag", etag);
+ context.setHeader("Server", "Sym File Server (v" + Server.VERSION + ")");
+ context.setHeader("Access-Control-Allow-Origin", "*");
+ final String ext = StringUtils.substringAfterLast(path, ".");
+ final String mimeType = MimeTypes.getMimeType(ext);
+ context.addHeader("Content-Type", mimeType);
+
+ if (etag.equals(ifNoneMatch)) {
+ context.addHeader("If-None-Match", "false");
+ context.setStatus(304);
+ } else {
+ context.addHeader("If-None-Match", "true");
+ }
+
+ response.sendBytes(data);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Gets a file failed", e);
+ }
+ }
+
+ /**
+ * Uploads file.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/upload", method = HttpMethod.POST)
+ public void uploadFile(final RequestContext context) {
+ final JSONObject result = Results.newFail();
+ context.renderJSONPretty(result);
+
+ final Request request = context.getRequest();
+ final int maxSize = (int) Symphonys.UPLOAD_FILE_MAX;
+
+ final Map succMap = new HashMap<>();
+ final List allFiles = request.getFileUploads("file[]");
+ final List files = new ArrayList<>();
+ String fileName;
+
+ Auth auth;
+ UploadManager uploadManager = null;
+ String uploadToken = null;
+ if (QN_ENABLED) {
+ auth = Auth.create(Symphonys.UPLOAD_QINIU_AK, Symphonys.UPLOAD_QINIU_SK);
+ uploadToken = auth.uploadToken(Symphonys.UPLOAD_QINIU_BUCKET);
+ uploadManager = new UploadManager(new Configuration());
+ }
+
+ final JSONObject data = new JSONObject();
+ final List errFiles = new ArrayList<>();
+
+ boolean checkFailed = false;
+ String suffix = "";
+ final String[] allowedSuffixArray = Symphonys.UPLOAD_SUFFIX.split(",");
+ for (final FileUpload file : allFiles) {
+ suffix = Headers.getSuffix(file);
+ if (!Strings.containsIgnoreCase(suffix, allowedSuffixArray)) {
+ checkFailed = true;
+
+ break;
+ }
+
+ if (maxSize < file.getData().length) {
+ continue;
+ }
+
+ files.add(file);
+ }
+
+ if (checkFailed) {
+ for (final FileUpload file : allFiles) {
+ fileName = file.getFilename();
+ errFiles.add(fileName);
+ }
+
+ data.put("errFiles", errFiles);
+ data.put("succMap", succMap);
+ result.put(Common.DATA, data);
+ result.put(Keys.CODE, 1);
+ String msg = langPropsService.get("invalidFileSuffixLabel");
+ msg = StringUtils.replace(msg, "${suffix}", suffix);
+ result.put(Keys.MSG, msg);
+
+ return;
+ }
+
+ final List fileBytes = new ArrayList<>();
+ if (Symphonys.QN_ENABLED) { // 文件上传性能优化 https://github.com/b3log/symphony/issues/866
+ for (final FileUpload file : files) {
+ final byte[] bytes = file.getData();
+ fileBytes.add(bytes);
+ }
+ }
+
+ final CountDownLatch countDownLatch = new CountDownLatch(files.size());
+ for (int i = 0; i < files.size(); i++) {
+ final FileUpload file = files.get(i);
+ final String originalName = fileName = Escapes.sanitizeFilename(file.getFilename());
+ try {
+ String url;
+ byte[] bytes;
+ suffix = Headers.getSuffix(file);
+ final String name = StringUtils.substringBeforeLast(fileName, ".");
+ final String uuid = StringUtils.substring(UUID.randomUUID().toString().replaceAll("-", ""), 0, 8);
+ fileName = name + '-' + uuid + "." + suffix;
+ fileName = genFilePath(fileName);
+ if (QN_ENABLED) {
+ bytes = fileBytes.get(i);
+ final String contentType = file.getContentType();
+ uploadManager.asyncPut(bytes, fileName, uploadToken, null, contentType, false, (key, r) -> {
+ LOGGER.log(Level.TRACE, "Uploaded [" + key + "], response [" + r.toString() + "]");
+ countDownLatch.countDown();
+ });
+ url = Symphonys.UPLOAD_QINIU_DOMAIN + "/" + fileName;
+ succMap.put(originalName, url);
+ } else {
+ final Path path = Paths.get(Symphonys.UPLOAD_LOCAL_DIR, fileName);
+ path.getParent().toFile().mkdirs();
+ try (final OutputStream output = new FileOutputStream(path.toFile())) {
+ IOUtils.write(file.getData(), output);
+ countDownLatch.countDown();
+ }
+ url = Latkes.getServePath() + "/upload/" + fileName;
+ succMap.put(originalName, url);
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Uploads file failed", e);
+
+ errFiles.add(originalName);
+ }
+ }
+
+ try {
+ countDownLatch.await(1, TimeUnit.MINUTES);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Count down latch failed", e);
+ }
+
+ data.put("errFiles", errFiles);
+ data.put("succMap", succMap);
+ result.put(Common.DATA, data);
+ result.put(Keys.CODE, StatusCodes.SUCC);
+ result.put(Keys.MSG, "");
+ }
+
+ /**
+ * Generates upload file path for the specified file name.
+ *
+ * @param fileName the specified file name
+ * @return "yyyy/MM/fileName"
+ */
+ public static String genFilePath(final String fileName) {
+ final String date = DateFormatUtils.format(System.currentTimeMillis(), "yyyy/MM");
+
+ return date + "/" + fileName;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/FollowProcessor.java b/src/main/java/org/b3log/symphony/processor/FollowProcessor.java
new file mode 100644
index 000000000..eead66b96
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/FollowProcessor.java
@@ -0,0 +1,354 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Follow;
+import org.b3log.symphony.model.Notification;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionCheck;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.service.FollowMgmtService;
+import org.b3log.symphony.service.NotificationMgmtService;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Follow processor.
+ *
+ * Follows a user (/follow/user), POST
+ * Unfollows a user (/follow/user), POST
+ * Follows a tag (/follow/tag), POST
+ * Unfollows a tag (/follow/tag), POST
+ * Follows an article (/follow/article), POST
+ * Unfollows an article (/follow/article), POST
+ * Watches an article (/follow/article-watch), POST
+ * Unwatches an article (/follow/article-watch), POST
+ *
+ *
+ * @author Liang Ding
+ * @version 1.3.0.5, Jul 22, 2018
+ * @since 0.2.5
+ */
+@RequestProcessor
+public class FollowProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(FollowProcessor.class);
+ /**
+ * Holds follows.
+ */
+ private static final Set FOLLOWS = new HashSet<>();
+ /**
+ * Follow management service.
+ */
+ @Inject
+ private FollowMgmtService followMgmtService;
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Follows a user.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/follow/user", method = HttpMethod.POST)
+ @Before(LoginCheck.class)
+ public void followUser(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingUserId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.followUser(followerUserId, followingUserId);
+
+ if (!FOLLOWS.contains(followingUserId + followerUserId)) {
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, followingUserId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, followerUserId);
+
+ notificationMgmtService.addNewFollowerNotification(notification);
+ }
+
+ FOLLOWS.add(followingUserId + followerUserId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Unfollows a user.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/unfollow/user", method = HttpMethod.POST)
+ @Before(LoginCheck.class)
+ public void unfollowUser(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingUserId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.unfollowUser(followerUserId, followingUserId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Follows a tag.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/follow/tag", method = HttpMethod.POST)
+ @Before(LoginCheck.class)
+ public void followTag(final RequestContext context) {
+ context.renderJSON();
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingTagId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.followTag(followerUserId, followingTagId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Unfollows a tag.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/unfollow/tag", method = HttpMethod.POST)
+ @Before(LoginCheck.class)
+ public void unfollowTag(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingTagId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.unfollowTag(followerUserId, followingTagId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Follows an article.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/follow/article", method = HttpMethod.POST)
+ @Before({LoginCheck.class, PermissionCheck.class})
+ public void followArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingArticleId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.followArticle(followerUserId, followingArticleId);
+
+ final JSONObject article = articleQueryService.getArticle(followingArticleId);
+ final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+
+ if (!FOLLOWS.contains(articleAuthorId + followingArticleId + "-" + followerUserId) &&
+ !articleAuthorId.equals(followerUserId)) {
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, articleAuthorId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, followingArticleId + "-" + followerUserId);
+
+ notificationMgmtService.addArticleNewFollowerNotification(notification);
+ }
+
+ FOLLOWS.add(articleAuthorId + followingArticleId + "-" + followerUserId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Unfollows an article.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/unfollow/article", method = HttpMethod.POST)
+ @Before(LoginCheck.class)
+ public void unfollowArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingArticleId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.unfollowArticle(followerUserId, followingArticleId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Watches an article.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/follow/article-watch", method = HttpMethod.POST)
+ @Before({LoginCheck.class, PermissionCheck.class})
+ public void watchArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingArticleId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.watchArticle(followerUserId, followingArticleId);
+
+ final JSONObject article = articleQueryService.getArticle(followingArticleId);
+ final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+
+ if (!FOLLOWS.contains(articleAuthorId + followingArticleId + "-" + followerUserId) &&
+ !articleAuthorId.equals(followerUserId)) {
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, articleAuthorId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, followingArticleId + "-" + followerUserId);
+
+ notificationMgmtService.addArticleNewWatcherNotification(notification);
+ }
+
+ FOLLOWS.add(articleAuthorId + followingArticleId + "-" + followerUserId);
+
+ context.renderTrueResult();
+ }
+
+ /**
+ * Unwatches an article.
+ *
+ * The request json object:
+ *
+ * {
+ * "followingId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/unfollow/article-watch", method = HttpMethod.POST)
+ @Before(LoginCheck.class)
+ public void unwatchArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String followingArticleId = requestJSONObject.optString(Follow.FOLLOWING_ID);
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ followMgmtService.unwatchArticle(followerUserId, followingArticleId);
+
+ context.renderTrueResult();
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ForwardProcessor.java b/src/main/java/org/b3log/symphony/processor/ForwardProcessor.java
new file mode 100644
index 000000000..b7cff2f3a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ForwardProcessor.java
@@ -0,0 +1,109 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.DataModelService;
+import org.b3log.symphony.service.LinkMgmtService;
+import org.b3log.symphony.util.Headers;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Map;
+
+/**
+ * Forward processor.
+ *
+ * Shows forward page (/forward), GET
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.0.5, Jan 17, 2019
+ * @since 2.3.0
+ */
+@RequestProcessor
+public class ForwardProcessor {
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Link management service.
+ */
+ @Inject
+ private LinkMgmtService linkMgmtService;
+
+ /**
+ * Shows jump page.
+ *
+ * @param context the specified context
+ * @throws Exception exception
+ */
+ @RequestProcessing(value = "/forward", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showForward(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ String to = context.param(Common.GOTO);
+ if (StringUtils.isBlank(to)) {
+ to = Latkes.getServePath();
+ }
+
+ final String referer = Headers.getHeader(request, "referer", "");
+ if (!StringUtils.startsWith(referer, Latkes.getServePath())) {
+ context.sendRedirect(Latkes.getServePath());
+
+ return;
+ }
+
+ final String url = to;
+ Symphonys.EXECUTOR_SERVICE.submit(() -> linkMgmtService.addLink(url));
+
+ final JSONObject user = Sessions.getUser();
+ if (null != user && UserExt.USER_XXX_STATUS_C_DISABLED == user.optInt(UserExt.USER_FORWARD_PAGE_STATUS)) {
+ context.sendRedirect(to);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "forward.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put("forwardURL", to);
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/IndexProcessor.java b/src/main/java/org/b3log/symphony/processor/IndexProcessor.java
new file mode 100644
index 000000000..6cb88164c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/IndexProcessor.java
@@ -0,0 +1,475 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Locales;
+import org.b3log.latke.util.Paginator;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.service.DataModelService;
+import org.b3log.symphony.service.UserMgmtService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.*;
+
+/**
+ * Index processor.
+ *
+ * Shows index (/), GET
+ * Show recent articles (/recent), GET
+ * Show question articles (/qna), GET
+ * Show watch relevant pages (/watch/*), GET
+ * Show hot articles (/hot), GET
+ * Show perfect articles (/perfect), GET
+ * Shows about (/about), GET
+ * Shows kill browser (/kill-browser), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Liyuan Li
+ * @version 1.15.0.3, Jan 8, 2019
+ * @since 0.2.0
+ */
+@RequestProcessor
+public class IndexProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(IndexProcessor.class);
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Shows question articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/qna", "/qna/unanswered", "/qna/reward", "/qna/hot"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showQnA(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "qna.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+ }
+
+ String sortModeStr = StringUtils.substringAfter(context.requestURI(), "/qna");
+ int sortMode;
+ switch (sortModeStr) {
+ case "":
+ sortMode = 0;
+
+ break;
+ case "/unanswered":
+ sortMode = 1;
+
+ break;
+ case "/reward":
+ sortMode = 2;
+
+ break;
+ case "/hot":
+ sortMode = 3;
+
+ break;
+ default:
+ sortMode = 0;
+ }
+
+ dataModel.put(Common.SELECTED, Common.QNA);
+ final JSONObject result = articleQueryService.getQuestionArticles(sortMode, pageNum, pageSize);
+ final List allArticles = (List) result.get(Article.ARTICLES);
+ dataModel.put(Common.LATEST_ARTICLES, allArticles);
+
+ final JSONObject pagination = result.getJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final List pageNums = (List) pagination.get(Pagination.PAGINATION_PAGE_NUMS);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put(Common.CURRENT, StringUtils.substringAfter(context.requestURI(), "/qna"));
+ }
+
+ /**
+ * Shows watch articles or users.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/watch", "/watch/users"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showWatch(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "watch.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+ }
+
+ dataModel.put(Common.WATCHING_ARTICLES, Collections.emptyList());
+ String sortModeStr = StringUtils.substringAfter(context.requestURI(), "/watch");
+ switch (sortModeStr) {
+ case "":
+ if (null != user) {
+ final List followingTagArticles = articleQueryService.getFollowingTagArticles(user.optString(Keys.OBJECT_ID), 1, pageSize);
+ dataModel.put(Common.WATCHING_ARTICLES, followingTagArticles);
+ }
+
+ break;
+ case "/users":
+ if (null != user) {
+ final List followingUserArticles = articleQueryService.getFollowingUserArticles(user.optString(Keys.OBJECT_ID), 1, pageSize);
+ dataModel.put(Common.WATCHING_ARTICLES, followingUserArticles);
+ }
+
+ break;
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put(Common.SELECTED, Common.WATCH);
+ dataModel.put(Common.CURRENT, StringUtils.substringAfter(context.requestURI(), "/watch"));
+ }
+
+ /**
+ * Shows index.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"", "/"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showIndex(final RequestContext context) {
+ final JSONObject currentUser = Sessions.getUser();
+ if (null != currentUser) {
+ // 自定义首页跳转 https://github.com/b3log/symphony/issues/774
+ final String indexRedirectURL = currentUser.optString(UserExt.USER_INDEX_REDIRECT_URL);
+ if (StringUtils.isNotBlank(indexRedirectURL)) {
+ context.sendRedirect(indexRedirectURL);
+
+ return;
+ }
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "index.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final List recentArticles = articleQueryService.getIndexRecentArticles();
+ dataModel.put(Common.RECENT_ARTICLES, recentArticles);
+
+ final List perfectArticles = articleQueryService.getIndexPerfectArticles();
+ dataModel.put(Common.PERFECT_ARTICLES, perfectArticles);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillIndexTags(dataModel);
+
+ dataModel.put(Common.SELECTED, Common.INDEX);
+ }
+
+ /**
+ * Shows recent articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/recent", "/recent/hot", "/recent/good", "/recent/reply"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showRecent(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "recent.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+ }
+
+ String sortModeStr = StringUtils.substringAfter(context.requestURI(), "/recent");
+ int sortMode;
+ switch (sortModeStr) {
+ case "":
+ sortMode = 0;
+
+ break;
+ case "/hot":
+ sortMode = 1;
+
+ break;
+ case "/good":
+ sortMode = 2;
+
+ break;
+ case "/reply":
+ sortMode = 3;
+
+ break;
+ default:
+ sortMode = 0;
+ }
+
+ dataModel.put(Common.SELECTED, Common.RECENT);
+ final JSONObject result = articleQueryService.getRecentArticles(sortMode, pageNum, pageSize);
+ final List allArticles = (List) result.get(Article.ARTICLES);
+ final List stickArticles = new ArrayList<>();
+ final Iterator iterator = allArticles.iterator();
+ while (iterator.hasNext()) {
+ final JSONObject article = iterator.next();
+ final boolean stick = article.optInt(Article.ARTICLE_T_STICK_REMAINS) > 0;
+ article.put(Article.ARTICLE_T_IS_STICK, stick);
+ if (stick) {
+ stickArticles.add(article);
+ iterator.remove();
+ }
+ }
+
+ dataModel.put(Common.STICK_ARTICLES, stickArticles);
+ dataModel.put(Common.LATEST_ARTICLES, allArticles);
+
+ final JSONObject pagination = result.getJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final List pageNums = (List) pagination.get(Pagination.PAGINATION_PAGE_NUMS);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put(Common.CURRENT, StringUtils.substringAfter(context.requestURI(), "/recent"));
+ }
+
+ /**
+ * Shows hot articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/hot", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHotArticles(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "hot.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+ }
+
+ final List indexArticles = articleQueryService.getHotArticles(pageSize);
+ dataModel.put(Common.INDEX_ARTICLES, indexArticles);
+ dataModel.put(Common.SELECTED, Common.HOT);
+
+ Stopwatchs.start("Fills");
+ try {
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+
+ /**
+ * Shows perfect articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/perfect", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showPerfectArticles(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "perfect.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+ }
+
+ final JSONObject result = articleQueryService.getPerfectArticles(pageNum, pageSize);
+ final List perfectArticles = (List) result.get(Article.ARTICLES);
+ dataModel.put(Common.PERFECT_ARTICLES, perfectArticles);
+ dataModel.put(Common.SELECTED, Common.PERFECT);
+ final JSONObject pagination = result.getJSONObject(Pagination.PAGINATION);
+ final int pageCount = pagination.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ final List pageNums = (List) pagination.get(Pagination.PAGINATION_PAGE_NUMS);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Shows about.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/about", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After(StopwatchEndAdvice.class)
+ public void showAbout(final RequestContext context) {
+ // 关于页主要描述社区愿景、行为准则、内容协议等,并介绍社区的功能
+ // 这些内容请搭建后自行编写发布,然后再修改这里进行重定向
+ context.sendRedirect(Latkes.getServePath() + "/member/admin");
+ }
+
+ /**
+ * Shows kill browser page with the specified context.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/kill-browser", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After(StopwatchEndAdvice.class)
+ public void showKillBrowser(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "other/kill-browser.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final Map langs = langPropsService.getAll(Locales.getLocale());
+ dataModel.putAll(langs);
+ Keys.fillRuntime(dataModel);
+ dataModelService.fillMinified(dataModel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/LoginProcessor.java b/src/main/java/org/b3log/symphony/processor/LoginProcessor.java
new file mode 100644
index 000000000..020a97116
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/LoginProcessor.java
@@ -0,0 +1,758 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.Response;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.latke.util.Locales;
+import org.b3log.latke.util.Requests;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.CSRFToken;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.UserForgetPwdValidation;
+import org.b3log.symphony.processor.advice.validate.UserRegister2Validation;
+import org.b3log.symphony.processor.advice.validate.UserRegisterValidation;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Login/Register processor.
+ *
+ * Registration (/register), GET/POST
+ * Login (/login), GET/POST
+ * Logout (/logout), GET
+ * Reset password (/reset-pwd), GET/POST
+ *
+ *
+ * @author Liang Ding
+ * @author Liyuan Li
+ * @version 1.13.12.8, Sep 6, 2019
+ * @since 0.2.0
+ */
+@RequestProcessor
+public class LoginProcessor {
+
+ /**
+ * Wrong password tries.
+ *
+ * <userId, {"wrongCount": int, "captcha": ""}>
+ *
+ */
+ public static final Map WRONG_PWD_TRIES = new ConcurrentHashMap<>();
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(LoginProcessor.class);
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Pointtransfer management service.
+ */
+ @Inject
+ private PointtransferMgmtService pointtransferMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Verifycode management service.
+ */
+ @Inject
+ private VerifycodeMgmtService verifycodeMgmtService;
+
+ /**
+ * Verifycode query service.
+ */
+ @Inject
+ private VerifycodeQueryService verifycodeQueryService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Invitecode query service.
+ */
+ @Inject
+ private InvitecodeQueryService invitecodeQueryService;
+
+ /**
+ * Invitecode management service.
+ */
+ @Inject
+ private InvitecodeMgmtService invitecodeMgmtService;
+
+ /**
+ * Invitecode management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ /**
+ * Tag query service.
+ */
+ @Inject
+ private TagQueryService tagQueryService;
+
+ /**
+ * Next guide step.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/guide/next", method = HttpMethod.POST)
+ @Before({LoginCheck.class})
+ public void nextGuideStep(final RequestContext context) {
+ context.renderJSON();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ } catch (final Exception e) {
+ LOGGER.warn(e.getMessage());
+
+ return;
+ }
+
+ JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+
+ int step = requestJSONObject.optInt(UserExt.USER_GUIDE_STEP);
+
+ if (UserExt.USER_GUIDE_STEP_STAR_PROJECT < step || UserExt.USER_GUIDE_STEP_FIN >= step) {
+ step = UserExt.USER_GUIDE_STEP_FIN;
+ }
+
+ try {
+ user = userQueryService.getUser(userId);
+ user.put(UserExt.USER_GUIDE_STEP, step);
+ userMgmtService.updateUser(userId, user);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Guide next step [" + step + "] failed", e);
+
+ return;
+ }
+
+ context.renderJSON(true);
+ }
+
+ /**
+ * Shows guide page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/guide", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showGuide(final RequestContext context) {
+ final JSONObject currentUser = Sessions.getUser();
+ final int step = currentUser.optInt(UserExt.USER_GUIDE_STEP);
+ if (UserExt.USER_GUIDE_STEP_FIN == step) {
+ context.sendRedirect(Latkes.getServePath());
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "verify/guide.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Common.CURRENT_USER, currentUser);
+
+ final List tags = tagQueryService.getTags(32);
+ dataModel.put(Tag.TAGS, tags);
+
+ final List users = userQueryService.getNiceUsers(6);
+ final Iterator iterator = users.iterator();
+ while (iterator.hasNext()) {
+ final JSONObject user = iterator.next();
+ if (user.optString(Keys.OBJECT_ID).equals(currentUser.optString(Keys.OBJECT_ID))) {
+ iterator.remove();
+
+ break;
+ }
+ }
+ dataModel.put(User.USERS, users);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows login page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/login", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showLogin(final RequestContext context) {
+ if (Sessions.isLoggedIn()) {
+ context.sendRedirect(Latkes.getServePath());
+
+ return;
+ }
+
+ String referer = context.param(Common.GOTO);
+ if (StringUtils.isBlank(referer)) {
+ referer = context.header("referer");
+ }
+
+ if (!StringUtils.startsWith(referer, Latkes.getServePath())) {
+ referer = Latkes.getServePath();
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "verify/login.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Common.GOTO, referer);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows forget password page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/forget-pwd", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showForgetPwd(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "verify/forget-pwd.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Forget password.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/forget-pwd", method = HttpMethod.POST)
+ @Before(UserForgetPwdValidation.class)
+ public void forgetPwd(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+ final String email = requestJSONObject.optString(User.USER_EMAIL);
+
+ try {
+ final JSONObject user = userQueryService.getUserByEmail(email);
+ if (null == user || UserExt.USER_STATUS_C_VALID != user.optInt(UserExt.USER_STATUS)) {
+ context.renderFalseResult().renderMsg(langPropsService.get("notFoundUserLabel"));
+
+ return;
+ }
+
+ final String userId = user.optString(Keys.OBJECT_ID);
+
+ final JSONObject verifycode = new JSONObject();
+ verifycode.put(Verifycode.BIZ_TYPE, Verifycode.BIZ_TYPE_C_RESET_PWD);
+ final String code = RandomStringUtils.randomAlphanumeric(6);
+ verifycode.put(Verifycode.CODE, code);
+ verifycode.put(Verifycode.EXPIRED, DateUtils.addDays(new Date(), 1).getTime());
+ verifycode.put(Verifycode.RECEIVER, email);
+ verifycode.put(Verifycode.STATUS, Verifycode.STATUS_C_UNSENT);
+ verifycode.put(Verifycode.TYPE, Verifycode.TYPE_C_EMAIL);
+ verifycode.put(Verifycode.USER_ID, userId);
+ verifycodeMgmtService.addVerifycode(verifycode);
+
+ context.renderTrueResult().renderMsg(langPropsService.get("verifycodeSentLabel"));
+ } catch (final ServiceException e) {
+ final String msg = langPropsService.get("resetPwdLabel") + " - " + e.getMessage();
+ LOGGER.log(Level.ERROR, msg + "[email=" + email + "]");
+
+ context.renderMsg(msg);
+ }
+ }
+
+ /**
+ * Shows reset password page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/reset-pwd", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showResetPwd(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, null);
+ context.setRenderer(renderer);
+ final Map dataModel = renderer.getDataModel();
+
+ final String code = context.param("code");
+ final JSONObject verifycode = verifycodeQueryService.getVerifycode(code);
+ if (null == verifycode) {
+ dataModel.put(Keys.MSG, langPropsService.get("verifycodeExpiredLabel"));
+ renderer.setTemplateName("error/custom.ftl");
+ } else {
+ renderer.setTemplateName("verify/reset-pwd.ftl");
+
+ final String userId = verifycode.optString(Verifycode.USER_ID);
+ final JSONObject user = userQueryService.getUser(userId);
+ dataModel.put(User.USER, user);
+ dataModel.put(Keys.CODE, code);
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Resets password.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/reset-pwd", method = HttpMethod.POST)
+ public void resetPwd(final RequestContext context) {
+ context.renderJSON();
+
+ final Response response = context.getResponse();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String password = requestJSONObject.optString(User.USER_PASSWORD); // Hashed
+ final String userId = requestJSONObject.optString(UserExt.USER_T_ID);
+ final String code = requestJSONObject.optString(Keys.CODE);
+ final JSONObject verifycode = verifycodeQueryService.getVerifycode(code);
+ if (null == verifycode || !verifycode.optString(Verifycode.USER_ID).equals(userId)) {
+ context.renderMsg(langPropsService.get("verifycodeExpiredLabel"));
+
+ return;
+ }
+
+ String name = null;
+ String email = null;
+ try {
+ final JSONObject user = userQueryService.getUser(userId);
+ if (null == user || UserExt.USER_STATUS_C_VALID != user.optInt(UserExt.USER_STATUS)) {
+ context.renderMsg(langPropsService.get("resetPwdLabel") + " - " + "User Not Found");
+
+ return;
+ }
+
+ user.put(User.USER_PASSWORD, password);
+ userMgmtService.updatePassword(user);
+ verifycodeMgmtService.removeByCode(code);
+ context.renderTrueResult();
+ LOGGER.info("User [email=" + user.optString(User.USER_EMAIL) + "] reseted password");
+
+ Sessions.login(response, userId, true);
+ } catch (final ServiceException e) {
+ final String msg = langPropsService.get("resetPwdLabel") + " - " + e.getMessage();
+ LOGGER.log(Level.ERROR, msg + "[name={0}, email={1}]", name, email);
+
+ context.renderMsg(msg);
+ }
+ }
+
+ /**
+ * Shows registration page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/register", method = HttpMethod.GET)
+ @Before(StopwatchStartAdvice.class)
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showRegister(final RequestContext context) {
+ if (Sessions.isLoggedIn()) {
+ context.sendRedirect(Latkes.getServePath());
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, null);
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Common.REFERRAL, "");
+
+ boolean useInvitationLink = false;
+
+ String referral = context.param("r");
+ if (!UserRegisterValidation.invalidUserName(referral)) {
+ final JSONObject referralUser = userQueryService.getUserByName(referral);
+ if (null != referralUser) {
+ dataModel.put(Common.REFERRAL, referral);
+
+ final Map permissions =
+ roleQueryService.getUserPermissionsGrantMap(referralUser.optString(Keys.OBJECT_ID));
+ final JSONObject useILPermission =
+ permissions.get(Permission.PERMISSION_ID_C_COMMON_USE_INVITATION_LINK);
+ useInvitationLink = useILPermission.optBoolean(Permission.PERMISSION_T_GRANT);
+ }
+ }
+
+ final String code = context.param("code");
+ if (StringUtils.isBlank(code)) { // Register Step 1
+ renderer.setTemplateName("verify/register.ftl");
+ } else { // Register Step 2
+ final JSONObject verifycode = verifycodeQueryService.getVerifycode(code);
+ if (null == verifycode) {
+ dataModel.put(Keys.MSG, langPropsService.get("verifycodeExpiredLabel"));
+ renderer.setTemplateName("error/custom.ftl");
+ } else {
+ renderer.setTemplateName("verify/register2.ftl");
+
+ final String userId = verifycode.optString(Verifycode.USER_ID);
+ final JSONObject user = userQueryService.getUser(userId);
+ dataModel.put(User.USER, user);
+
+ if (UserExt.USER_STATUS_C_VALID == user.optInt(UserExt.USER_STATUS)
+ || UserExt.NULL_USER_NAME.equals(user.optString(User.USER_NAME))) {
+ dataModel.put(Keys.MSG, langPropsService.get("userExistLabel"));
+ renderer.setTemplateName("error/custom.ftl");
+ } else {
+ referral = StringUtils.substringAfter(code, "r=");
+ if (StringUtils.isNotBlank(referral)) {
+ dataModel.put(Common.REFERRAL, referral);
+ }
+ }
+ }
+ }
+
+ final String allowRegister = optionQueryService.getAllowRegister();
+ dataModel.put(Option.ID_C_MISC_ALLOW_REGISTER, allowRegister);
+ if (useInvitationLink && "2".equals(allowRegister)) {
+ dataModel.put(Option.ID_C_MISC_ALLOW_REGISTER, "1");
+ }
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Register Step 1.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/register", method = HttpMethod.POST)
+ @Before(UserRegisterValidation.class)
+ public void register(final RequestContext context) {
+ context.renderJSON();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+ final String name = requestJSONObject.optString(User.USER_NAME);
+ final String email = requestJSONObject.optString(User.USER_EMAIL);
+ final String invitecode = requestJSONObject.optString(Invitecode.INVITECODE);
+ final String referral = requestJSONObject.optString(Common.REFERRAL);
+
+ final JSONObject user = new JSONObject();
+ user.put(User.USER_NAME, name);
+ user.put(User.USER_EMAIL, email);
+ user.put(User.USER_PASSWORD, "");
+ final Locale locale = Locales.getLocale();
+ user.put(UserExt.USER_LANGUAGE, locale.getLanguage() + "_" + locale.getCountry());
+
+ try {
+ final String newUserId = userMgmtService.addUser(user);
+
+ final JSONObject verifycode = new JSONObject();
+ verifycode.put(Verifycode.BIZ_TYPE, Verifycode.BIZ_TYPE_C_REGISTER);
+ String code = RandomStringUtils.randomAlphanumeric(6);
+ if (StringUtils.isNotBlank(referral)) {
+ code += "r=" + referral;
+ }
+ verifycode.put(Verifycode.CODE, code);
+ verifycode.put(Verifycode.EXPIRED, DateUtils.addDays(new Date(), 1).getTime());
+ verifycode.put(Verifycode.RECEIVER, email);
+ verifycode.put(Verifycode.STATUS, Verifycode.STATUS_C_UNSENT);
+ verifycode.put(Verifycode.TYPE, Verifycode.TYPE_C_EMAIL);
+ verifycode.put(Verifycode.USER_ID, newUserId);
+ verifycodeMgmtService.addVerifycode(verifycode);
+
+ final String allowRegister = optionQueryService.getAllowRegister();
+ if ("2".equals(allowRegister) && StringUtils.isNotBlank(invitecode)) {
+ final JSONObject ic = invitecodeQueryService.getInvitecode(invitecode);
+ ic.put(Invitecode.USER_ID, newUserId);
+ ic.put(Invitecode.USE_TIME, System.currentTimeMillis());
+ final String icId = ic.optString(Keys.OBJECT_ID);
+
+ invitecodeMgmtService.updateInvitecode(icId, ic);
+ }
+
+ context.renderTrueResult().renderMsg(langPropsService.get("verifycodeSentLabel"));
+ } catch (final ServiceException e) {
+ final String msg = langPropsService.get("registerFailLabel") + " - " + e.getMessage();
+ LOGGER.log(Level.ERROR, msg + "[name={0}, email={1}]", name, email);
+
+ context.renderMsg(msg);
+ }
+ }
+
+ /**
+ * Register Step 2.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/register2", method = HttpMethod.POST)
+ @Before(UserRegister2Validation.class)
+ public void register2(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final Response response = context.getResponse();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+
+ final String password = requestJSONObject.optString(User.USER_PASSWORD); // Hashed
+ final int appRole = requestJSONObject.optInt(UserExt.USER_APP_ROLE);
+ final String referral = requestJSONObject.optString(Common.REFERRAL);
+ final String userId = requestJSONObject.optString(UserExt.USER_T_ID);
+
+ String name = null;
+ String email = null;
+ try {
+ final JSONObject user = userQueryService.getUser(userId);
+ if (null == user) {
+ context.renderMsg(langPropsService.get("registerFailLabel") + " - " + "User Not Found");
+
+ return;
+ }
+
+ name = user.optString(User.USER_NAME);
+ email = user.optString(User.USER_EMAIL);
+
+ user.put(UserExt.USER_APP_ROLE, appRole);
+ user.put(User.USER_PASSWORD, password);
+ user.put(UserExt.USER_STATUS, UserExt.USER_STATUS_C_VALID);
+
+ userMgmtService.addUser(user);
+
+ Sessions.login(response, userId, false);
+
+ final String ip = Requests.getRemoteAddr(request);
+ userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), ip, true, true);
+
+ if (StringUtils.isNotBlank(referral) && !UserRegisterValidation.invalidUserName(referral)) {
+ final JSONObject referralUser = userQueryService.getUserByName(referral);
+ if (null != referralUser) {
+ final String referralId = referralUser.optString(Keys.OBJECT_ID);
+ pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_INVITED_REGISTER,
+ Pointtransfer.TRANSFER_SUM_C_INVITE_REGISTER, referralId, System.currentTimeMillis(), "");
+ pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, referralId,
+ Pointtransfer.TRANSFER_TYPE_C_INVITE_REGISTER,
+ Pointtransfer.TRANSFER_SUM_C_INVITE_REGISTER, userId, System.currentTimeMillis(), "");
+
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, referralId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, userId);
+ notificationMgmtService.addInvitationLinkUsedNotification(notification);
+ }
+ }
+
+ final JSONObject ic = invitecodeQueryService.getInvitecodeByUserId(userId);
+ if (null != ic && Invitecode.STATUS_C_UNUSED == ic.optInt(Invitecode.STATUS)) {
+ ic.put(Invitecode.STATUS, Invitecode.STATUS_C_USED);
+ ic.put(Invitecode.USER_ID, userId);
+ ic.put(Invitecode.USE_TIME, System.currentTimeMillis());
+ final String icId = ic.optString(Keys.OBJECT_ID);
+
+ invitecodeMgmtService.updateInvitecode(icId, ic);
+
+ final String icGeneratorId = ic.optString(Invitecode.GENERATOR_ID);
+ if (StringUtils.isNotBlank(icGeneratorId) && !Pointtransfer.ID_C_SYS.equals(icGeneratorId)) {
+ pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, icGeneratorId,
+ Pointtransfer.TRANSFER_TYPE_C_INVITECODE_USED,
+ Pointtransfer.TRANSFER_SUM_C_INVITECODE_USED, userId, System.currentTimeMillis(), "");
+
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, icGeneratorId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, userId);
+
+ notificationMgmtService.addInvitecodeUsedNotification(notification);
+ }
+ }
+
+ context.renderTrueResult();
+
+ LOGGER.log(Level.INFO, "Registered a user [name={0}, email={1}]", name, email);
+ } catch (final ServiceException e) {
+ final String msg = langPropsService.get("registerFailLabel") + " - " + e.getMessage();
+ LOGGER.log(Level.ERROR, msg + " [name={0}, email={1}]", name, email);
+
+ context.renderMsg(msg);
+ }
+ }
+
+ /**
+ * Logins user.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/login", method = HttpMethod.POST)
+ public void login(final RequestContext context) {
+ final Request request = context.getRequest();
+ final Response response = context.getResponse();
+
+ context.renderJSON().renderMsg(langPropsService.get("loginFailLabel"));
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ } catch (final Exception e) {
+ context.renderMsg(langPropsService.get("paramsParseFailedLabel"));
+
+ return;
+ }
+
+ final String nameOrEmail = requestJSONObject.optString("nameOrEmail");
+
+ try {
+ JSONObject user = userQueryService.getUserByName(nameOrEmail);
+ if (null == user) {
+ user = userQueryService.getUserByEmail(nameOrEmail);
+ }
+
+ if (null == user) {
+ context.renderMsg(langPropsService.get("notFoundUserLabel"));
+
+ return;
+ }
+
+ if (UserExt.USER_STATUS_C_INVALID == user.optInt(UserExt.USER_STATUS)) {
+ userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), "", false, true);
+ context.renderMsg(langPropsService.get("userBlockLabel"));
+
+ return;
+ }
+
+ if (UserExt.USER_STATUS_C_NOT_VERIFIED == user.optInt(UserExt.USER_STATUS)) {
+ userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), "", false, true);
+ context.renderMsg(langPropsService.get("notVerifiedLabel"));
+
+ return;
+ }
+
+ if (UserExt.USER_STATUS_C_INVALID_LOGIN == user.optInt(UserExt.USER_STATUS)
+ || UserExt.USER_STATUS_C_DEACTIVATED == user.optInt(UserExt.USER_STATUS)) {
+ userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), "", false, true);
+ context.renderMsg(langPropsService.get("invalidLoginLabel"));
+
+ return;
+ }
+
+ final String userId = user.optString(Keys.OBJECT_ID);
+ JSONObject wrong = WRONG_PWD_TRIES.get(userId);
+ if (null == wrong) {
+ wrong = new JSONObject();
+ }
+
+ final int wrongCount = wrong.optInt(Common.WRON_COUNT);
+ if (wrongCount > 3) {
+ final String captcha = requestJSONObject.optString(CaptchaProcessor.CAPTCHA);
+ if (!StringUtils.equals(wrong.optString(CaptchaProcessor.CAPTCHA), captcha)) {
+ context.renderMsg(langPropsService.get("captchaErrorLabel"));
+ context.renderJSONValue(Common.NEED_CAPTCHA, userId);
+
+ return;
+ }
+ }
+
+ final String userPassword = user.optString(User.USER_PASSWORD);
+ if (userPassword.equals(requestJSONObject.optString(User.USER_PASSWORD))) {
+ final String token = Sessions.login(response, userId, requestJSONObject.optBoolean(Common.REMEMBER_LOGIN));
+
+ final String ip = Requests.getRemoteAddr(request);
+ userMgmtService.updateOnlineStatus(user.optString(Keys.OBJECT_ID), ip, true, true);
+
+ context.renderMsg("").renderTrueResult();
+ context.renderJSONValue(Keys.TOKEN, token);
+
+ WRONG_PWD_TRIES.remove(userId);
+
+ return;
+ }
+
+ if (wrongCount > 2) {
+ context.renderJSONValue(Common.NEED_CAPTCHA, userId);
+ }
+
+ wrong.put(Common.WRON_COUNT, wrongCount + 1);
+ WRONG_PWD_TRIES.put(userId, wrong);
+
+ context.renderMsg(langPropsService.get("wrongPwdLabel"));
+ } catch (final ServiceException e) {
+ context.renderMsg(langPropsService.get("loginFailLabel"));
+ }
+ }
+
+ /**
+ * Logout.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/logout", method = HttpMethod.GET)
+ public void logout(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ Sessions.logout(user.optString(Keys.OBJECT_ID), context.getResponse());
+ }
+
+ String destinationURL = context.param(Common.GOTO);
+ if (StringUtils.isBlank(destinationURL)) {
+ destinationURL = context.header("referer");
+ }
+
+ context.sendRedirect(destinationURL);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/NotificationProcessor.java b/src/main/java/org/b3log/symphony/processor/NotificationProcessor.java
new file mode 100644
index 000000000..008f75c56
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/NotificationProcessor.java
@@ -0,0 +1,766 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.util.Paginator;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.DataModelService;
+import org.b3log.symphony.service.NotificationMgmtService;
+import org.b3log.symphony.service.NotificationQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.*;
+
+/**
+ * Notification processor.
+ *
+ * Shows [commented] notifications (/notifications/commented), GET
+ * Shows [reply] notifications (/notifications/reply), GET
+ * Shows [at] notifications (/notifications/at), GET
+ * Shows [following] notifications (/notifications/following), GET
+ * Shows [point] notifications (/notifications/point), GET
+ * Shows [broadcast] notifications (/notifications/broadcast), GET
+ * Shows [sysAnnounce] notifications (/notifications/sys-announce), GET
+ * Makes article/comment read (/notifications/read), GET
+ * Gets unread count of notifications (/notifications/unread/count), GET
+ * Makes all notifications as read (/notifications/all-read), GET
+ * Makes the specified type notifications as read (/notifications/read/{type}), GET
+ * Removes a notification (/notifications/remove), POST
+ * Remove notifications by the specified type (/notifications/remove/{type}), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Liyuan Li
+ * @version 1.12.0.4, Jan 5, 2019
+ * @since 0.2.5
+ */
+@RequestProcessor
+public class NotificationProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(NotificationProcessor.class);
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Notification query service.
+ */
+ @Inject
+ private NotificationQueryService notificationQueryService;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Remove notifications by the specified type.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/remove/{type}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeNotifications(final RequestContext context) {
+ final String type = context.pathVar("type"); // commented/reply/at/following/point/broadcast
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ switch (type) {
+ case "commented":
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_COMMENTED);
+
+ break;
+ case "reply":
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_REPLY);
+
+ break;
+ case "at":
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_AT);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_FOLLOWER);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_WATCHER);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_UP);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_DOWN);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_UP);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_DOWN);
+
+ break;
+ case "following":
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_FOLLOWING_USER);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_FOLLOWING_ARTICLE_UPDATE);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_FOLLOWING_ARTICLE_COMMENT);
+
+ break;
+ case "point":
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_ARTICLE_REWARD);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_ARTICLE_THANK);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_CHARGE);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_COMMENT_ACCEPT);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_COMMENT_THANK);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_EXCHANGE);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_PERFECT_ARTICLE);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_POINT_TRANSFER);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_ABUSE_POINT_DEDUCT);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_INVITECODE_USED);
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_INVITATION_LINK_USED);
+
+ break;
+ case "broadcast":
+ notificationMgmtService.removeNotifications(userId, Notification.DATA_TYPE_C_BROADCAST);
+
+ break;
+ default:
+ context.renderJSON(false);
+
+ return;
+ }
+
+ context.renderJSON(true);
+ }
+
+ /**
+ * Removes a notification.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/remove", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void removeNotification(final RequestContext context) {
+ context.renderJSON(true);
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final String notificationId = requestJSONObject.optString(Common.ID);
+
+ final JSONObject notification = notificationQueryService.getNotification(notificationId);
+ if (null == notification) {
+ return;
+ }
+
+ if (!notification.optString(Notification.NOTIFICATION_USER_ID).equals(userId)) {
+ return;
+ }
+
+ notificationMgmtService.removeNotification(notificationId);
+ }
+
+ /**
+ * Shows [sysAnnounce] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/sys-announce", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showSysAnnounceNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+ final JSONObject currentUser = Sessions.getUser();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/sys-announce.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getSysAnnounceNotifications(userId, pageNum, pageSize);
+ final List notifications = (List) result.get(Keys.RESULTS);
+
+ dataModel.put(Common.SYS_ANNOUNCE_NOTIFICATIONS, notifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Makes all notifications as read.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/all-read", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void makeAllNotificationsRead(final RequestContext context) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ notificationMgmtService.makeAllRead(userId);
+
+ context.renderJSON(true);
+ }
+
+ /**
+ * Makes the specified type notifications as read.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/read/{type}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void makeNotificationReadByType(final RequestContext context) {
+ final String type = context.pathVar("type"); // "commented"/"at"/"following"
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ switch (type) {
+ case "commented":
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_COMMENTED);
+
+ break;
+ case "reply":
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_REPLY);
+
+ break;
+ case "at":
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_AT);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_FOLLOWER);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_WATCHER);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_UP);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_DOWN);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_UP);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_DOWN);
+
+ break;
+ case "following":
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_FOLLOWING_USER);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_FOLLOWING_ARTICLE_UPDATE);
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_FOLLOWING_ARTICLE_COMMENT);
+
+ break;
+ default:
+ context.renderJSON(false);
+
+ return;
+ }
+
+
+ context.renderJSON(true);
+ }
+
+ /**
+ * Makes article/comment read.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/read", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void makeNotificationRead(final RequestContext context) {
+ final JSONObject requestJSONObject = context.requestJSON();
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final String articleId = requestJSONObject.optString(Article.ARTICLE_T_ID);
+ final List commentIds = Arrays.asList(requestJSONObject.optString(Comment.COMMENT_T_IDS).split(","));
+
+ notificationMgmtService.makeRead(userId, articleId, commentIds);
+
+ context.renderJSON(true);
+ }
+
+ /**
+ * Navigates notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void navigateNotifications(final RequestContext context) {
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ final int unreadCommentedNotificationCnt = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_COMMENTED);
+ if (unreadCommentedNotificationCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/commented");
+
+ return;
+ }
+
+ final int unreadReplyNotificationCnt = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_REPLY);
+ if (unreadReplyNotificationCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/reply");
+
+ return;
+ }
+
+ final int unreadAtNotificationCnt
+ = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_AT)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_FOLLOWER)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_WATCHER)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_UP)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_DOWN)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_UP)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_DOWN);
+ if (unreadAtNotificationCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/at");
+
+ return;
+ }
+
+ final int unreadPointNotificationCnt = notificationQueryService.getUnreadPointNotificationCount(userId);
+ if (unreadPointNotificationCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/point");
+
+ return;
+ }
+
+ final int unreadFollowingNotificationCnt = notificationQueryService.getUnreadFollowingNotificationCount(userId);
+ if (unreadFollowingNotificationCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/following");
+
+ return;
+ }
+
+ final int unreadBroadcastCnt
+ = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_BROADCAST);
+ if (unreadBroadcastCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/broadcast");
+
+ return;
+ }
+
+ final int unreadSysAnnounceCnt = notificationQueryService.getUnreadSysAnnounceNotificationCount(userId);
+ if (unreadSysAnnounceCnt > 0) {
+ context.sendRedirect(Latkes.getServePath() + "/notifications/sys-announce");
+
+ return;
+ }
+
+ context.sendRedirect(Latkes.getServePath() + "/notifications/commented");
+ }
+
+ /**
+ * Shows [point] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/point", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showPointNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/point.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getPointNotifications(userId, pageNum, pageSize);
+ final List pointNotifications = (List) result.get(Keys.RESULTS);
+ dataModel.put(Common.POINT_NOTIFICATIONS, pointNotifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ notificationMgmtService.makeRead(pointNotifications);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Fills notification count.
+ *
+ * @param userId the specified user id
+ * @param dataModel the specified data model
+ */
+ private void fillNotificationCount(final String userId, final Map dataModel) {
+ final int unreadCommentedNotificationCnt = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_COMMENTED);
+ dataModel.put(Common.UNREAD_COMMENTED_NOTIFICATION_CNT, unreadCommentedNotificationCnt);
+
+ final int unreadReplyNotificationCnt = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_REPLY);
+ dataModel.put(Common.UNREAD_REPLY_NOTIFICATION_CNT, unreadReplyNotificationCnt);
+
+ final int unreadAtNotificationCnt
+ = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_AT)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_FOLLOWER)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_NEW_WATCHER)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_UP)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_COMMENT_VOTE_DOWN)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_UP)
+ + notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_ARTICLE_VOTE_DOWN);
+ dataModel.put(Common.UNREAD_AT_NOTIFICATION_CNT, unreadAtNotificationCnt);
+
+ final int unreadFollowingNotificationCnt = notificationQueryService.getUnreadFollowingNotificationCount(userId);
+ dataModel.put(Common.UNREAD_FOLLOWING_NOTIFICATION_CNT, unreadFollowingNotificationCnt);
+
+ final int unreadPointNotificationCnt = notificationQueryService.getUnreadPointNotificationCount(userId);
+ dataModel.put(Common.UNREAD_POINT_NOTIFICATION_CNT, unreadPointNotificationCnt);
+
+ final int unreadBroadcastNotificationCnt = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_BROADCAST);
+ dataModel.put(Common.UNREAD_BROADCAST_NOTIFICATION_CNT, unreadBroadcastNotificationCnt);
+
+ final int unreadSysAnnounceNotificationCnt = notificationQueryService.getUnreadSysAnnounceNotificationCount(userId);
+ dataModel.put(Common.UNREAD_SYS_ANNOUNCE_NOTIFICATION_CNT, unreadSysAnnounceNotificationCnt);
+
+ final int unreadNewFollowerNotificationCnt = notificationQueryService.getUnreadNotificationCountByType(userId, Notification.DATA_TYPE_C_NEW_FOLLOWER);
+ dataModel.put(Common.UNREAD_NEW_FOLLOWER_NOTIFICATION_CNT, unreadNewFollowerNotificationCnt);
+
+ dataModel.put(Common.UNREAD_NOTIFICATION_CNT, unreadAtNotificationCnt + unreadBroadcastNotificationCnt
+ + unreadCommentedNotificationCnt + unreadFollowingNotificationCnt + unreadPointNotificationCnt
+ + unreadReplyNotificationCnt + unreadSysAnnounceNotificationCnt + unreadNewFollowerNotificationCnt);
+ }
+
+ /**
+ * Shows [commented] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/commented", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showCommentedNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/commented.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getCommentedNotifications(userId, pageNum, pageSize);
+ final List commentedNotifications = (List) result.get(Keys.RESULTS);
+ dataModel.put(Common.COMMENTED_NOTIFICATIONS, commentedNotifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows [reply] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/reply", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showReplyNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/reply.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getReplyNotifications(userId, pageNum, pageSize);
+ final List replyNotifications = (List) result.get(Keys.RESULTS);
+ dataModel.put(Common.REPLY_NOTIFICATIONS, replyNotifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows [at] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/at", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAtNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/at.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getAtNotifications(userId, pageNum, pageSize);
+ final List atNotifications = (List) result.get(Keys.RESULTS);
+
+ dataModel.put(Common.AT_NOTIFICATIONS, atNotifications);
+
+ final List articleFollowAndWatchNotifications = new ArrayList<>();
+ for (final JSONObject notification : atNotifications) {
+ if (Notification.DATA_TYPE_C_AT != notification.optInt(Notification.NOTIFICATION_DATA_TYPE)) {
+ articleFollowAndWatchNotifications.add(notification);
+ }
+ }
+ notificationMgmtService.makeRead(articleFollowAndWatchNotifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows [following] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/following", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showFollowingNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/following.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getFollowingNotifications(userId, pageNum, pageSize);
+ final List followingNotifications = (List) result.get(Keys.RESULTS);
+
+ dataModel.put(Common.FOLLOWING_NOTIFICATIONS, followingNotifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Shows [broadcast] notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/broadcast", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showBroadcastNotifications(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ context.sendError(403);
+
+ return;
+ }
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/notifications/broadcast.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.NOTIFICATION_LIST_CNT;
+ final int windowSize = Symphonys.NOTIFICATION_LIST_WIN_SIZE;
+
+ final JSONObject result = notificationQueryService.getBroadcastNotifications(userId, pageNum, pageSize);
+ final List broadcastNotifications = (List) result.get(Keys.RESULTS);
+
+ dataModel.put(Common.BROADCAST_NOTIFICATIONS, broadcastNotifications);
+
+ fillNotificationCount(userId, dataModel);
+
+ final int recordCnt = result.getInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) recordCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Gets unread count of notifications.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/notifications/unread/count", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({StopwatchEndAdvice.class})
+ public void getUnreadNotificationCount(final RequestContext context) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final Map dataModel = new HashMap<>();
+
+ fillNotificationCount(userId, dataModel);
+
+ context.renderJSON(new JSONObject(dataModel)).renderTrueResult().
+ renderJSONValue(UserExt.USER_NOTIFY_STATUS, currentUser.optInt(UserExt.USER_NOTIFY_STATUS));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/ReportProcessor.java b/src/main/java/org/b3log/symphony/processor/ReportProcessor.java
new file mode 100644
index 000000000..33d2153a0
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/ReportProcessor.java
@@ -0,0 +1,114 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.symphony.model.Report;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.ReportMgmtService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.StatusCodes;
+import org.json.JSONObject;
+
+/**
+ * Report processor.
+ *
+ * Reports content or users (/report), POST
+ *
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jun 26, 2018
+ * @since 3.1.0
+ */
+@RequestProcessor
+public class ReportProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ReportProcessor.class);
+
+ /**
+ * Report management service.
+ */
+ @Inject
+ private ReportMgmtService reportMgmtService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Reports content or users.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/report", method = HttpMethod.POST)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After(StopwatchEndAdvice.class)
+ public void report(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final String dataId = requestJSONObject.optString(Report.REPORT_DATA_ID);
+ final int dataType = requestJSONObject.optInt(Report.REPORT_DATA_TYPE);
+ final int type = requestJSONObject.optInt(Report.REPORT_TYPE);
+ final String memo = StringUtils.trim(requestJSONObject.optString(Report.REPORT_MEMO));
+
+ final JSONObject report = new JSONObject();
+ report.put(Report.REPORT_USER_ID, userId);
+ report.put(Report.REPORT_DATA_ID, dataId);
+ report.put(Report.REPORT_DATA_TYPE, dataType);
+ report.put(Report.REPORT_TYPE, type);
+ report.put(Report.REPORT_MEMO, memo);
+
+ try {
+ reportMgmtService.addReport(report);
+
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.SUCC);
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Adds a report failed", e);
+
+ context.renderMsg(langPropsService.get("systemErrLabel"));
+ context.renderJSONValue(Keys.STATUS_CODE, StatusCodes.ERR);
+ }
+ }
+
+}
diff --git a/src/main/java/org/b3log/symphony/processor/SearchProcessor.java b/src/main/java/org/b3log/symphony/processor/SearchProcessor.java
new file mode 100644
index 000000000..302bdcb66
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/SearchProcessor.java
@@ -0,0 +1,205 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Paginator;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.service.DataModelService;
+import org.b3log.symphony.service.SearchQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Escapes;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Search processor.
+ *
+ * Searches keyword (/search), GET
+ *
+ *
+ * @author Liang Ding
+ * @version 1.1.4.1, Dec 8, 2017
+ * @since 1.4.0
+ */
+@RequestProcessor
+public class SearchProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(SearchProcessor.class);
+
+ /**
+ * Search query service.
+ */
+ @Inject
+ private SearchQueryService searchQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Searches.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/search", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void search(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "search-articles.ftl");
+
+ if (!Symphonys.ES_ENABLED && !Symphonys.ALGOLIA_ENABLED) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final Map dataModel = renderer.getDataModel();
+ String keyword = context.param("key");
+ if (StringUtils.isBlank(keyword)) {
+ keyword = "";
+ }
+ dataModel.put(Common.KEY, Escapes.escapeHTML(keyword));
+
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+ }
+ final List articles = new ArrayList<>();
+ int total = 0;
+
+ if (Symphonys.ES_ENABLED) {
+ final JSONObject result = searchQueryService.searchElasticsearch(Article.ARTICLE, keyword, pageNum, pageSize);
+ if (null == result || 0 != result.optInt("status")) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONObject hitsResult = result.optJSONObject("hits");
+ final JSONArray hits = hitsResult.optJSONArray("hits");
+
+ for (int i = 0; i < hits.length(); i++) {
+ final JSONObject article = hits.optJSONObject(i).optJSONObject("_source");
+ articles.add(article);
+ }
+
+ total = result.optInt("total");
+ }
+
+ if (Symphonys.ALGOLIA_ENABLED) {
+ final JSONObject result = searchQueryService.searchAlgolia(keyword, pageNum, pageSize);
+ if (null == result) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final JSONArray hits = result.optJSONArray("hits");
+
+ for (int i = 0; i < hits.length(); i++) {
+ final JSONObject article = hits.optJSONObject(i);
+ articles.add(article);
+ }
+
+ total = result.optInt("nbHits");
+ if (total > 1000) {
+ total = 1000; // Algolia limits the maximum number of search results to 1000
+ }
+ }
+
+ articleQueryService.organizeArticles(articles);
+ final Integer participantsCnt = Symphonys.ARTICLE_LIST_PARTICIPANTS_CNT;
+ articleQueryService.genParticipants(articles, participantsCnt);
+
+ dataModel.put(Article.ARTICLES, articles);
+
+ final int pageCount = (int) Math.ceil(total / (double) pageSize);
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, Symphonys.ARTICLE_LIST_WIN_SIZE);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ String searchEmptyLabel = langPropsService.get("searchEmptyLabel");
+ searchEmptyLabel = searchEmptyLabel.replace("${key}", keyword);
+ dataModel.put("searchEmptyLabel", searchEmptyLabel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/SettingsProcessor.java b/src/main/java/org/b3log/symphony/processor/SettingsProcessor.java
new file mode 100644
index 000000000..f7f8bdd60
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/SettingsProcessor.java
@@ -0,0 +1,1159 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.Response;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.ServiceException;
+import org.b3log.latke.util.Strings;
+import org.b3log.latke.util.TimeZones;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.*;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.processor.advice.validate.PointTransferValidation;
+import org.b3log.symphony.processor.advice.validate.UpdatePasswordValidation;
+import org.b3log.symphony.processor.advice.validate.UpdateProfilesValidation;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Escapes;
+import org.b3log.symphony.util.Languages;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.*;
+
+/**
+ * Settings processor.
+ *
+ * Shows settings (/settings), GET
+ * Shows settings pages (/settings/{page}), GET
+ * Updates profiles (/settings/profiles), POST
+ * Updates user avatar (/settings/avatar), POST
+ * Geo status (/settings/geo/status), POST
+ * Privacy (/settings/privacy), POST
+ * Function (/settings/function), POST
+ * Transfer point (/point/transfer), POST
+ * Queries invitecode state (/invitecode/state), GET
+ * Point buy invitecode (/point/buy-invitecode), POST
+ * Exports posts(article/comment) to a file (/export/posts), POST
+ * Updates emotions (/settings/emotionList), POST
+ * Password (/settings/password), POST
+ * Updates i18n (/settings/i18n), POST
+ * Sends email verify code (/settings/email/vc), POST
+ * Updates email (/settings/email), POST
+ * Updates username (/settings/username), POST
+ * Deactivates user (/settings/deactivate), POST
+ *
+ *
+ * @author Liang Ding
+ * @version 1.3.2.4, Sep 6, 2019
+ * @since 2.4.0
+ */
+@RequestProcessor
+public class SettingsProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(SettingsProcessor.class);
+
+ /**
+ * Post export service.
+ */
+ @Inject
+ private PostExportService postExportService;
+
+ /**
+ * Invitecode query service.
+ */
+ @Inject
+ private InvitecodeQueryService invitecodeQueryService;
+
+ /**
+ * Invitecode management service.
+ */
+ @Inject
+ private InvitecodeMgmtService invitecodeMgmtService;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Emotion query service.
+ */
+ @Inject
+ private EmotionQueryService emotionQueryService;
+
+ /**
+ * Emotion management service.
+ */
+ @Inject
+ private EmotionMgmtService emotionMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Avatar query service.
+ */
+ @Inject
+ private AvatarQueryService avatarQueryService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ /**
+ * Verifycode query service.
+ */
+ @Inject
+ private VerifycodeQueryService verifycodeQueryService;
+
+ /**
+ * Verifycode management service.
+ */
+ @Inject
+ private VerifycodeMgmtService verifycodeMgmtService;
+
+ /**
+ * Pointtransfer management service.
+ */
+ @Inject
+ private PointtransferMgmtService pointtransferMgmtService;
+
+ /**
+ * Deactivates user.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/deactivate", method = HttpMethod.POST)
+ @Before({LoginCheck.class})
+ public void deactivateUser(final RequestContext context) {
+ context.renderJSON();
+
+ final Response response = context.getResponse();
+ final JSONObject currentUser = Sessions.getUser();
+ try {
+ userMgmtService.deactivateUser(currentUser.optString(Keys.OBJECT_ID));
+ Sessions.logout(currentUser.optString(Keys.OBJECT_ID), response);
+
+ context.renderTrueResult();
+ } catch (final Exception e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates username.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/username", method = HttpMethod.POST)
+ @Before({LoginCheck.class})
+ public void updateUserName(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ try {
+ if (currentUser.optInt(UserExt.USER_POINT) < Pointtransfer.TRANSFER_SUM_C_CHANGE_USERNAME) {
+ throw new ServiceException(langPropsService.get("insufficientBalanceLabel"));
+ }
+
+ final JSONObject user = userQueryService.getUser(userId);
+ final String oldName = user.optString(User.USER_NAME);
+ final String newName = requestJSONObject.optString(User.USER_NAME);
+ user.put(User.USER_NAME, newName);
+
+ userMgmtService.updateUserName(userId, user);
+
+ pointtransferMgmtService.transfer(userId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_CHANGE_USERNAME, Pointtransfer.TRANSFER_SUM_C_CHANGE_USERNAME,
+ oldName + "-" + newName, System.currentTimeMillis(), "");
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Sends email verify code.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/email/vc", method = HttpMethod.POST)
+ @Before({LoginCheck.class})
+ public void sendEmailVC(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String email = StringUtils.lowerCase(StringUtils.trim(requestJSONObject.optString(User.USER_EMAIL)));
+ if (!Strings.isEmail(email)) {
+ final String msg = langPropsService.get("sendFailedLabel") + " - " + langPropsService.get("invalidEmailLabel");
+ context.renderMsg(msg);
+
+ return;
+ }
+
+ final String captcha = requestJSONObject.optString(CaptchaProcessor.CAPTCHA);
+ if (CaptchaProcessor.invalidCaptcha(captcha)) {
+ final String msg = langPropsService.get("sendFailedLabel") + " - " + langPropsService.get("captchaErrorLabel");
+ context.renderMsg(msg);
+
+ return;
+ }
+
+ final JSONObject user = Sessions.getUser();
+ if (email.equalsIgnoreCase(user.optString(User.USER_EMAIL))) {
+ final String msg = langPropsService.get("sendFailedLabel") + " - " + langPropsService.get("bindedLabel");
+ context.renderMsg(msg);
+
+ return;
+ }
+
+ final String userId = user.optString(Keys.OBJECT_ID);
+ try {
+ JSONObject verifycode = verifycodeQueryService.getVerifycodeByUserId(Verifycode.TYPE_C_EMAIL, Verifycode.BIZ_TYPE_C_BIND_EMAIL, userId);
+ if (null != verifycode) {
+ context.renderTrueResult().renderMsg(langPropsService.get("vcSentLabel"));
+
+ return;
+ }
+
+ if (null != userQueryService.getUserByEmail(email)) {
+ context.renderMsg(langPropsService.get("duplicatedEmailLabel"));
+
+ return;
+ }
+
+ final String code = RandomStringUtils.randomNumeric(6);
+ verifycode = new JSONObject();
+ verifycode.put(Verifycode.USER_ID, userId);
+ verifycode.put(Verifycode.BIZ_TYPE, Verifycode.BIZ_TYPE_C_BIND_EMAIL);
+ verifycode.put(Verifycode.TYPE, Verifycode.TYPE_C_EMAIL);
+ verifycode.put(Verifycode.CODE, code);
+ verifycode.put(Verifycode.STATUS, Verifycode.STATUS_C_UNSENT);
+ verifycode.put(Verifycode.EXPIRED, DateUtils.addMinutes(new Date(), 10).getTime());
+ verifycode.put(Verifycode.RECEIVER, email);
+ verifycodeMgmtService.addVerifycode(verifycode);
+
+ context.renderTrueResult().renderMsg(langPropsService.get("verifycodeSentLabel"));
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates email.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/email", method = HttpMethod.POST)
+ @Before({LoginCheck.class})
+ public void updateEmail(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String captcha = requestJSONObject.optString(CaptchaProcessor.CAPTCHA);
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ try {
+ final JSONObject verifycode = verifycodeQueryService.getVerifycodeByUserId(Verifycode.TYPE_C_EMAIL, Verifycode.BIZ_TYPE_C_BIND_EMAIL, userId);
+ if (null == verifycode) {
+ final String msg = langPropsService.get("updateFailLabel") + " - " + langPropsService.get("captchaErrorLabel");
+ context.renderMsg(msg);
+ context.renderJSONValue(Keys.CODE, 2);
+
+ return;
+ }
+
+ if (!StringUtils.equals(verifycode.optString(Verifycode.CODE), captcha)) {
+ final String msg = langPropsService.get("updateFailLabel") + " - " + langPropsService.get("captchaErrorLabel");
+ context.renderMsg(msg);
+ context.renderJSONValue(Keys.CODE, 2);
+
+ return;
+ }
+
+ final JSONObject user = userQueryService.getUser(userId);
+ final String email = verifycode.optString(Verifycode.RECEIVER);
+ user.put(User.USER_EMAIL, email);
+ userMgmtService.updateUserEmail(userId, user);
+ verifycodeMgmtService.removeByCode(captcha);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates user i18n.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/i18n", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class})
+ public void updateI18n(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ LOGGER.warn(e.getMessage());
+
+ requestJSONObject = new JSONObject();
+ }
+
+ String userLanguage = requestJSONObject.optString(UserExt.USER_LANGUAGE, Locale.SIMPLIFIED_CHINESE.toString());
+ if (!Languages.getAvailableLanguages().contains(userLanguage)) {
+ userLanguage = Locale.US.toString();
+ }
+
+ String userTimezone = requestJSONObject.optString(UserExt.USER_TIMEZONE, TimeZone.getDefault().getID());
+ if (!Arrays.asList(TimeZone.getAvailableIDs()).contains(userTimezone)) {
+ userTimezone = TimeZone.getDefault().getID();
+ }
+
+ try {
+ JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ user = userQueryService.getUser(userId);
+
+ user.put(UserExt.USER_LANGUAGE, userLanguage);
+ user.put(UserExt.USER_TIMEZONE, userTimezone);
+
+ userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Shows settings pages.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/settings", "/settings/{page}"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, LoginCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showSettings(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, null);
+ context.setRenderer(renderer);
+ String page = context.pathVar("page");
+ if (StringUtils.isBlank(page)) {
+ page = "profile";
+ }
+ page += ".ftl";
+ renderer.setTemplateName("home/settings/" + page);
+ final Map dataModel = renderer.getDataModel();
+
+ final JSONObject user = Sessions.getUser();
+ UserProcessor.fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final String userId = user.optString(Keys.OBJECT_ID);
+
+ final int invitedUserCount = userQueryService.getInvitedUserCount(userId);
+ dataModel.put(Common.INVITED_USER_COUNT, invitedUserCount);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ String inviteTipLabel = (String) dataModel.get("inviteTipLabel");
+ inviteTipLabel = inviteTipLabel.replace("{point}", String.valueOf(Pointtransfer.TRANSFER_SUM_C_INVITE_REGISTER));
+ dataModel.put("inviteTipLabel", inviteTipLabel);
+
+ String pointTransferTipLabel = (String) dataModel.get("pointTransferTipLabel");
+ pointTransferTipLabel = pointTransferTipLabel.replace("{point}", Symphonys.POINT_TRANSER_MIN + "");
+ dataModel.put("pointTransferTipLabel", pointTransferTipLabel);
+
+ String dataExportTipLabel = (String) dataModel.get("dataExportTipLabel");
+ dataExportTipLabel = dataExportTipLabel.replace("{point}",
+ String.valueOf(Pointtransfer.TRANSFER_SUM_C_DATA_EXPORT));
+ dataModel.put("dataExportTipLabel", dataExportTipLabel);
+
+ final String allowRegister = optionQueryService.getAllowRegister();
+ dataModel.put("allowRegister", allowRegister);
+
+ String buyInvitecodeLabel = langPropsService.get("buyInvitecodeLabel");
+ buyInvitecodeLabel = buyInvitecodeLabel.replace("${point}",
+ String.valueOf(Pointtransfer.TRANSFER_SUM_C_BUY_INVITECODE));
+ buyInvitecodeLabel = buyInvitecodeLabel.replace("${point2}",
+ String.valueOf(Pointtransfer.TRANSFER_SUM_C_INVITECODE_USED));
+ dataModel.put("buyInvitecodeLabel", buyInvitecodeLabel);
+
+ String updateNameTipLabel = (String) dataModel.get("updateNameTipLabel");
+ updateNameTipLabel = updateNameTipLabel.replace("{point}", Symphonys.POINT_CHANGE_USERNAME + "");
+ dataModel.put("updateNameTipLabel", updateNameTipLabel);
+
+ final List invitecodes = invitecodeQueryService.getValidInvitecodes(userId);
+ for (final JSONObject invitecode : invitecodes) {
+ String msg = langPropsService.get("expireTipLabel");
+ msg = msg.replace("${time}", DateFormatUtils.format(invitecode.optLong(Keys.OBJECT_ID)
+ + Symphonys.INVITECODE_EXPIRED, "yyyy-MM-dd HH:mm"));
+ invitecode.put(Common.MEMO, msg);
+ }
+
+ dataModel.put(Invitecode.INVITECODES, invitecodes);
+
+ final String requestURI = context.requestURI();
+ if (requestURI.contains("function")) {
+ dataModel.put(Emotion.EMOTIONS, emotionQueryService.getEmojis(userId));
+ dataModel.put(Emotion.SHORT_T_LIST, emojiLists);
+ }
+
+ if (requestURI.contains("i18n")) {
+ dataModel.put(Common.LANGUAGES, Languages.getAvailableLanguages());
+
+ final List timezones = new ArrayList<>();
+ final List timeZones = TimeZones.getInstance().getTimeZones();
+ for (final TimeZones.TimeZoneWithDisplayNames timeZone : timeZones) {
+ final JSONObject timezone = new JSONObject();
+
+ timezone.put(Common.ID, timeZone.getTimeZone().getID());
+ timezone.put(Common.NAME, timeZone.getDisplayName());
+
+ timezones.add(timezone);
+ }
+ dataModel.put(Common.TIMEZONES, timezones);
+ }
+
+ dataModel.put(Common.TYPE, "settings");
+
+ // “感谢加入”系统通知已读置位 https://github.com/b3log/symphony/issues/907
+ notificationMgmtService.makeRead(userId, Notification.DATA_TYPE_C_SYS_ANNOUNCE_NEW_USER);
+ }
+
+ /**
+ * Updates user geo status.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/geo/status", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class})
+ public void updateGeoStatus(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ LOGGER.warn(e.getMessage());
+
+ requestJSONObject = new JSONObject();
+ }
+
+ int geoStatus = requestJSONObject.optInt(UserExt.USER_GEO_STATUS);
+ if (UserExt.USER_GEO_STATUS_C_PRIVATE != geoStatus && UserExt.USER_GEO_STATUS_C_PUBLIC != geoStatus) {
+ geoStatus = UserExt.USER_GEO_STATUS_C_PUBLIC;
+ }
+
+ try {
+ JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ user = userQueryService.getUser(userId);
+ user.put(UserExt.USER_GEO_STATUS, geoStatus);
+
+ userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates user privacy.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/privacy", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class})
+ public void updatePrivacy(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ LOGGER.warn(e.getMessage());
+
+ requestJSONObject = new JSONObject();
+ }
+
+ final boolean articleStatus = requestJSONObject.optBoolean(UserExt.USER_ARTICLE_STATUS);
+ final boolean commentStatus = requestJSONObject.optBoolean(UserExt.USER_COMMENT_STATUS);
+ final boolean followingUserStatus = requestJSONObject.optBoolean(UserExt.USER_FOLLOWING_USER_STATUS);
+ final boolean followingTagStatus = requestJSONObject.optBoolean(UserExt.USER_FOLLOWING_TAG_STATUS);
+ final boolean followingArticleStatus = requestJSONObject.optBoolean(UserExt.USER_FOLLOWING_ARTICLE_STATUS);
+ final boolean watchingArticleStatus = requestJSONObject.optBoolean(UserExt.USER_WATCHING_ARTICLE_STATUS);
+ final boolean followerStatus = requestJSONObject.optBoolean(UserExt.USER_FOLLOWER_STATUS);
+ final boolean breezemoonStatus = requestJSONObject.optBoolean(UserExt.USER_BREEZEMOON_STATUS);
+ final boolean pointStatus = requestJSONObject.optBoolean(UserExt.USER_POINT_STATUS);
+ final boolean onlineStatus = requestJSONObject.optBoolean(UserExt.USER_ONLINE_STATUS);
+ final boolean uaStatus = requestJSONObject.optBoolean(UserExt.USER_UA_STATUS);
+ final boolean userJoinPointRank = requestJSONObject.optBoolean(UserExt.USER_JOIN_POINT_RANK);
+ final boolean userJoinUsedPointRank = requestJSONObject.optBoolean(UserExt.USER_JOIN_USED_POINT_RANK);
+
+ JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ user = userQueryService.getUser(userId);
+
+ user.put(UserExt.USER_ONLINE_STATUS, onlineStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_ARTICLE_STATUS, articleStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_COMMENT_STATUS, commentStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_FOLLOWING_USER_STATUS, followingUserStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_FOLLOWING_TAG_STATUS, followingTagStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_FOLLOWING_ARTICLE_STATUS, followingArticleStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_WATCHING_ARTICLE_STATUS, watchingArticleStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_FOLLOWER_STATUS, followerStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_BREEZEMOON_STATUS, breezemoonStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_POINT_STATUS, pointStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_UA_STATUS, uaStatus ? UserExt.USER_XXX_STATUS_C_PUBLIC : UserExt.USER_XXX_STATUS_C_PRIVATE);
+ user.put(UserExt.USER_JOIN_POINT_RANK, userJoinPointRank ? UserExt.USER_JOIN_XXX_C_JOIN : UserExt.USER_JOIN_XXX_C_NOT_JOIN);
+ user.put(UserExt.USER_JOIN_USED_POINT_RANK, userJoinUsedPointRank ? UserExt.USER_JOIN_XXX_C_JOIN : UserExt.USER_JOIN_XXX_C_NOT_JOIN);
+
+ try {
+ userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates user function.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/function", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class})
+ public void updateFunction(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ LOGGER.warn(e.getMessage());
+
+ requestJSONObject = new JSONObject();
+ }
+
+ String userListPageSizeStr = requestJSONObject.optString(UserExt.USER_LIST_PAGE_SIZE);
+ final int userCommentViewMode = requestJSONObject.optInt(UserExt.USER_COMMENT_VIEW_MODE);
+ final int userAvatarViewMode = requestJSONObject.optInt(UserExt.USER_AVATAR_VIEW_MODE);
+ final int userListViewMode = requestJSONObject.optInt(UserExt.USER_LIST_VIEW_MODE);
+ final boolean notifyStatus = requestJSONObject.optBoolean(UserExt.USER_NOTIFY_STATUS);
+ final boolean subMailStatus = requestJSONObject.optBoolean(UserExt.USER_SUB_MAIL_STATUS);
+ final boolean keyboardShortcutsStatus = requestJSONObject.optBoolean(UserExt.USER_KEYBOARD_SHORTCUTS_STATUS);
+ final boolean userReplyWatchArticleStatus = requestJSONObject.optBoolean(UserExt.USER_REPLY_WATCH_ARTICLE_STATUS);
+ final boolean forwardStatus = requestJSONObject.optBoolean(UserExt.USER_FORWARD_PAGE_STATUS);
+ String indexRedirectURL = requestJSONObject.optString(UserExt.USER_INDEX_REDIRECT_URL);
+ if (!Strings.isURL(indexRedirectURL)) {
+ indexRedirectURL = "";
+ }
+ if (StringUtils.isNotBlank(indexRedirectURL) && !StringUtils.startsWith(indexRedirectURL, Latkes.getServePath())) {
+ context.renderMsg(langPropsService.get("onlyInternalURLLabel"));
+
+ return;
+ }
+ if (StringUtils.isNotBlank(indexRedirectURL)) {
+ String tmp = StringUtils.substringBefore(indexRedirectURL, "?");
+ if (StringUtils.endsWith(tmp, "/")) {
+ tmp = StringUtils.substringBeforeLast(tmp, "/");
+ }
+ if (StringUtils.equalsIgnoreCase(tmp, Latkes.getServePath())) {
+ indexRedirectURL = "";
+ }
+ }
+
+ int userListPageSize;
+ try {
+ userListPageSize = Integer.valueOf(userListPageSizeStr);
+ if (10 > userListPageSize) {
+ userListPageSize = 10;
+ }
+ if (userListPageSize > 96) {
+ userListPageSize = 96;
+ }
+ } catch (final Exception e) {
+ userListPageSize = Symphonys.ARTICLE_LIST_CNT;
+ }
+
+ JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ user = userQueryService.getUser(userId);
+
+ user.put(UserExt.USER_LIST_PAGE_SIZE, userListPageSize);
+ user.put(UserExt.USER_COMMENT_VIEW_MODE, userCommentViewMode);
+ user.put(UserExt.USER_AVATAR_VIEW_MODE, userAvatarViewMode);
+ user.put(UserExt.USER_LIST_VIEW_MODE, userListViewMode);
+ user.put(UserExt.USER_NOTIFY_STATUS, notifyStatus ? UserExt.USER_XXX_STATUS_C_ENABLED : UserExt.USER_XXX_STATUS_C_DISABLED);
+ user.put(UserExt.USER_SUB_MAIL_STATUS, subMailStatus ? UserExt.USER_XXX_STATUS_C_ENABLED : UserExt.USER_XXX_STATUS_C_DISABLED);
+ user.put(UserExt.USER_KEYBOARD_SHORTCUTS_STATUS, keyboardShortcutsStatus ? UserExt.USER_XXX_STATUS_C_ENABLED : UserExt.USER_XXX_STATUS_C_DISABLED);
+ user.put(UserExt.USER_REPLY_WATCH_ARTICLE_STATUS, userReplyWatchArticleStatus ? UserExt.USER_XXX_STATUS_C_ENABLED : UserExt.USER_XXX_STATUS_C_DISABLED);
+ user.put(UserExt.USER_FORWARD_PAGE_STATUS, forwardStatus ? UserExt.USER_XXX_STATUS_C_ENABLED : UserExt.USER_XXX_STATUS_C_DISABLED);
+ user.put(UserExt.USER_INDEX_REDIRECT_URL, indexRedirectURL);
+
+ try {
+ userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates user profiles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/profiles", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, UpdateProfilesValidation.class})
+ public void updateProfiles(final RequestContext context) {
+ context.renderJSON();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+ final String userTags = requestJSONObject.optString(UserExt.USER_TAGS);
+ final String userURL = requestJSONObject.optString(User.USER_URL);
+ final String userQQ = requestJSONObject.optString(UserExt.USER_QQ);
+ String userIntro = StringUtils.trim(requestJSONObject.optString(UserExt.USER_INTRO));
+ userIntro = Escapes.escapeHTML(userIntro);
+ String userNickname = StringUtils.trim(requestJSONObject.optString(UserExt.USER_NICKNAME));
+ userNickname = Escapes.escapeHTML(userNickname);
+
+ final JSONObject user = Sessions.getUser();
+ user.put(UserExt.USER_TAGS, userTags);
+ user.put(User.USER_URL, userURL);
+ user.put(UserExt.USER_QQ, userQQ);
+ user.put(UserExt.USER_INTRO, userIntro);
+ user.put(UserExt.USER_NICKNAME, userNickname);
+ user.put(UserExt.USER_AVATAR_TYPE, UserExt.USER_AVATAR_TYPE_C_UPLOAD);
+
+ try {
+ userMgmtService.updateProfiles(user);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates user avatar.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/avatar", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, UpdateProfilesValidation.class})
+ public void updateAvatar(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+ final String userAvatarURL = requestJSONObject.optString(UserExt.USER_AVATAR_URL);
+
+ JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+ user = userQueryService.getUser(userId);
+ user.put(UserExt.USER_AVATAR_TYPE, UserExt.USER_AVATAR_TYPE_C_UPLOAD);
+ user.put(UserExt.USER_UPDATE_TIME, System.currentTimeMillis());
+
+ if (Strings.contains(userAvatarURL, new String[]{"<", ">", "\"", "'"})) {
+ user.put(UserExt.USER_AVATAR_URL, AvatarQueryService.DEFAULT_AVATAR_URL);
+ } else {
+ if (Symphonys.QN_ENABLED) {
+ final String qiniuDomain = Symphonys.UPLOAD_QINIU_DOMAIN;
+
+ if (!StringUtils.startsWith(userAvatarURL, qiniuDomain)) {
+ user.put(UserExt.USER_AVATAR_URL, AvatarQueryService.DEFAULT_AVATAR_URL);
+ } else {
+ user.put(UserExt.USER_AVATAR_URL, userAvatarURL);
+ }
+ } else {
+ user.put(UserExt.USER_AVATAR_URL, userAvatarURL);
+ }
+ }
+
+ try {
+ userMgmtService.updateUser(user.optString(Keys.OBJECT_ID), user);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ context.renderMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Updates user password.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/password", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, UpdatePasswordValidation.class})
+ public void updatePassword(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+
+ final String password = requestJSONObject.optString(User.USER_PASSWORD);
+ final String newPassword = requestJSONObject.optString(User.USER_NEW_PASSWORD);
+
+ final JSONObject user = Sessions.getUser();
+ if (!password.equals(user.optString(User.USER_PASSWORD))) {
+ context.renderMsg(langPropsService.get("invalidOldPwdLabel"));
+
+ return;
+ }
+
+ user.put(User.USER_PASSWORD, newPassword);
+
+ try {
+ userMgmtService.updatePassword(user);
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ final String msg = langPropsService.get("updateFailLabel") + " - " + e.getMessage();
+ LOGGER.log(Level.ERROR, msg, e);
+
+ context.renderMsg(msg);
+ }
+ }
+
+ /**
+ * Updates user emotions.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/settings/emotionList", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class})
+ public void updateEmoji(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String emotionList = requestJSONObject.optString(Emotion.EMOTIONS);
+
+ final JSONObject user = Sessions.getUser();
+ try {
+ emotionMgmtService.setEmotionList(user.optString(Keys.OBJECT_ID), emotionList);
+
+ context.renderTrueResult();
+ } catch (final ServiceException e) {
+ final String msg = langPropsService.get("updateFailLabel") + " - " + e.getMessage();
+ LOGGER.log(Level.ERROR, msg, e);
+
+ context.renderMsg(msg);
+ }
+ }
+
+ /**
+ * Point transfer.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/point/transfer", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, PointTransferValidation.class})
+ public void pointTransfer(final RequestContext context) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+ context.renderJSON(ret);
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = (JSONObject) context.attr(Keys.REQUEST);
+
+ final int amount = requestJSONObject.optInt(Common.AMOUNT);
+ final JSONObject toUser = (JSONObject) context.attr(Common.TO_USER);
+ final JSONObject currentUser = Sessions.getUser();
+ String memo = (String) context.attr(Pointtransfer.MEMO);
+ if (StringUtils.isBlank(memo)) {
+ memo = "";
+ }
+
+ final String fromId = currentUser.optString(Keys.OBJECT_ID);
+ final String toId = toUser.optString(Keys.OBJECT_ID);
+
+ final String transferId = pointtransferMgmtService.transfer(fromId, toId,
+ Pointtransfer.TRANSFER_TYPE_C_ACCOUNT2ACCOUNT, amount, toId, System.currentTimeMillis(), memo);
+ final boolean succ = null != transferId;
+ ret.put(Keys.STATUS_CODE, succ);
+ if (!succ) {
+ ret.put(Keys.MSG, langPropsService.get("transferFailLabel"));
+ } else {
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, toId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, transferId);
+
+ notificationMgmtService.addPointTransferNotification(notification);
+ }
+ }
+
+ /**
+ * Queries invitecode state.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/invitecode/state", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class})
+ public void queryInvitecode(final RequestContext context) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+ context.renderJSON(ret);
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ String invitecode = requestJSONObject.optString(Invitecode.INVITECODE);
+ if (StringUtils.isBlank(invitecode)) {
+ ret.put(Keys.STATUS_CODE, -1);
+ ret.put(Keys.MSG, invitecode + " " + langPropsService.get("notFoundInvitecodeLabel"));
+
+ return;
+ }
+
+ invitecode = invitecode.trim();
+
+ final JSONObject result = invitecodeQueryService.getInvitecode(invitecode);
+
+ if (null == result) {
+ ret.put(Keys.STATUS_CODE, -1);
+ ret.put(Keys.MSG, langPropsService.get("notFoundInvitecodeLabel"));
+ } else {
+ final int status = result.optInt(Invitecode.STATUS);
+ ret.put(Keys.STATUS_CODE, status);
+
+ switch (status) {
+ case Invitecode.STATUS_C_USED:
+ ret.put(Keys.MSG, langPropsService.get("invitecodeUsedLabel"));
+
+ break;
+ case Invitecode.STATUS_C_UNUSED:
+ String msg = langPropsService.get("invitecodeOkLabel");
+ msg = msg.replace("${time}", DateFormatUtils.format(result.optLong(Keys.OBJECT_ID)
+ + Symphonys.INVITECODE_EXPIRED, "yyyy-MM-dd HH:mm"));
+
+ ret.put(Keys.MSG, msg);
+
+ break;
+ case Invitecode.STATUS_C_STOPUSE:
+ ret.put(Keys.MSG, langPropsService.get("invitecodeStopLabel"));
+
+ break;
+ default:
+ ret.put(Keys.MSG, langPropsService.get("notFoundInvitecodeLabel"));
+ }
+ }
+ }
+
+ /**
+ * Point buy invitecode.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/point/buy-invitecode", method = HttpMethod.POST)
+ @Before({LoginCheck.class, CSRFCheck.class, PermissionCheck.class})
+ public void pointBuy(final RequestContext context) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+ context.renderJSON(ret);
+
+ final String allowRegister = optionQueryService.getAllowRegister();
+ if (!"2".equals(allowRegister)) {
+ return;
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String fromId = currentUser.optString(Keys.OBJECT_ID);
+ final String userName = currentUser.optString(User.USER_NAME);
+
+ // 故意先生成后返回校验,所以即使积分不够也是可以兑换成功的
+ // 这是为了让积分不够的用户可以通过这个后门兑换、分发邀请码以实现积分“自充”
+ // 后期可能会关掉这个【特性】
+ final String invitecode = invitecodeMgmtService.userGenInvitecode(fromId, userName);
+
+ final String transferId = pointtransferMgmtService.transfer(fromId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_BUY_INVITECODE, Pointtransfer.TRANSFER_SUM_C_BUY_INVITECODE,
+ invitecode, System.currentTimeMillis(), "");
+ final boolean succ = null != transferId;
+ ret.put(Keys.STATUS_CODE, succ);
+ if (!succ) {
+ ret.put(Keys.MSG, langPropsService.get("exchangeFailedLabel"));
+ } else {
+ String msg = langPropsService.get("expireTipLabel");
+ msg = msg.replace("${time}", DateFormatUtils.format(System.currentTimeMillis()
+ + Symphonys.INVITECODE_EXPIRED, "yyyy-MM-dd HH:mm"));
+ ret.put(Keys.MSG, invitecode + " " + msg);
+ }
+ }
+
+ /**
+ * Exports posts(article/comment) to a file.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/export/posts", method = HttpMethod.POST)
+ @Before({LoginCheck.class})
+ public void exportPosts(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject user = Sessions.getUser();
+ final String userId = user.optString(Keys.OBJECT_ID);
+
+ final String downloadURL = postExportService.exportPosts(userId);
+ if ("-1".equals(downloadURL)) {
+ context.renderJSONValue(Keys.MSG, langPropsService.get("insufficientBalanceLabel"));
+
+ } else if (StringUtils.isBlank(downloadURL)) {
+ return;
+ }
+
+ context.renderJSON(true).renderJSONValue("url", downloadURL);
+ }
+
+ private static final String[][] emojiLists = {{
+ "smile",
+ "laughing",
+ "smirk",
+ "heart_eyes",
+ "kissing_heart",
+ "flushed",
+ "grin",
+ "stuck_out_tongue_closed_eyes",
+ "kissing",
+ "sleeping",
+ "anguished",
+ "open_mouth",
+ "expressionless",
+ "unamused",
+ "sweat_smile",
+ "weary",
+ "sob",
+ "joy",
+ "astonished",
+ "scream"
+ }, {
+ "tired_face",
+ "rage",
+ "triumph",
+ "yum",
+ "mask",
+ "sunglasses",
+ "dizzy_face",
+ "imp",
+ "smiling_imp",
+ "innocent",
+ "alien",
+ "yellow_heart",
+ "blue_heart",
+ "purple_heart",
+ "heart",
+ "green_heart",
+ "broken_heart",
+ "dizzy",
+ "anger",
+ "exclamation"
+ }, {
+ "question",
+ "zzz",
+ "notes",
+ "poop",
+ "+1",
+ "-1",
+ "ok_hand",
+ "punch",
+ "v",
+ "hand",
+ "point_up",
+ "point_down",
+ "pray",
+ "clap",
+ "muscle",
+ "ok_woman",
+ "no_good",
+ "raising_hand",
+ "massage",
+ "haircut"
+ }, {
+ "nail_care",
+ "see_no_evil",
+ "feet",
+ "kiss",
+ "eyes",
+ "trollface",
+ "snowman",
+ "zap",
+ "cat",
+ "dog",
+ "mouse",
+ "hamster",
+ "rabbit",
+ "frog",
+ "koala",
+ "pig",
+ "monkey",
+ "racehorse",
+ "camel",
+ "sheep"
+ }, {
+ "elephant",
+ "panda_face",
+ "snake",
+ "hatched_chick",
+ "hatching_chick",
+ "turtle",
+ "bug",
+ "honeybee",
+ "beetle",
+ "snail",
+ "octopus",
+ "whale",
+ "dolphin",
+ "dragon",
+ "goat",
+ "paw_prints",
+ "tulip",
+ "four_leaf_clover",
+ "rose",
+ "mushroom"
+ }, {
+ "seedling",
+ "shell",
+ "crescent_moon",
+ "partly_sunny",
+ "octocat",
+ "jack_o_lantern",
+ "ghost",
+ "santa",
+ "tada",
+ "camera",
+ "loudspeaker",
+ "hourglass",
+ "lock",
+ "key",
+ "bulb",
+ "hammer",
+ "moneybag",
+ "smoking",
+ "bomb",
+ "gun"
+ }, {
+ "hocho",
+ "pill",
+ "syringe",
+ "scissors",
+ "swimmer",
+ "black_joker",
+ "coffee",
+ "tea",
+ "sake",
+ "beer",
+ "wine_glass",
+ "pizza",
+ "hamburger",
+ "poultry_leg",
+ "meat_on_bone",
+ "dango",
+ "doughnut",
+ "icecream",
+ "shaved_ice",
+ "cake"
+ }, {
+ "cookie",
+ "lollipop",
+ "apple",
+ "green_apple",
+ "tangerine",
+ "lemon",
+ "cherries",
+ "grapes",
+ "watermelon",
+ "strawberry",
+ "peach",
+ "melon",
+ "banana",
+ "pear",
+ "pineapple",
+ "sweet_potato",
+ "eggplant",
+ "tomato",
+ Emotion.EOF_EMOJI // 标记结束以便在function.ftl中处理
+ }};
+}
diff --git a/src/main/java/org/b3log/symphony/processor/SitemapProcessor.java b/src/main/java/org/b3log/symphony/processor/SitemapProcessor.java
new file mode 100644
index 000000000..1b2c23dfe
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/SitemapProcessor.java
@@ -0,0 +1,83 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.TextXmlRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.sitemap.Sitemap;
+import org.b3log.symphony.service.SitemapQueryService;
+
+/**
+ * Sitemap processor.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Sep 24, 2016
+ * @since 1.6.0
+ */
+@RequestProcessor
+public class SitemapProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(SitemapProcessor.class);
+
+ /**
+ * Sitemap query service.
+ */
+ @Inject
+ private SitemapQueryService sitemapQueryService;
+
+ /**
+ * Returns the sitemap.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/sitemap.xml", method = HttpMethod.GET)
+ public void sitemap(final RequestContext context) {
+ final TextXmlRenderer renderer = new TextXmlRenderer();
+
+ context.setRenderer(renderer);
+
+ final Sitemap sitemap = new Sitemap();
+
+ try {
+ LOGGER.log(Level.INFO, "Generating sitemap....");
+
+ sitemapQueryService.genIndex(sitemap);
+ sitemapQueryService.genDomains(sitemap);
+ sitemapQueryService.genArticles(sitemap);
+
+ final String content = sitemap.toString();
+
+ LOGGER.log(Level.INFO, "Generated sitemap");
+
+ renderer.setContent(content);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Get blog article feed error", e);
+
+ context.getResponse().sendError(500);
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/SkinRenderer.java b/src/main/java/org/b3log/symphony/processor/SkinRenderer.java
new file mode 100644
index 000000000..4a04f6f50
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/SkinRenderer.java
@@ -0,0 +1,174 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import freemarker.template.Template;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.util.Locales;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.b3log.symphony.util.Templates;
+import org.json.JSONObject;
+
+import java.io.StringWriter;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Skin user-switchable FreeMarker Renderer.
+ *
+ * @author Liang Ding
+ * @version 1.3.0.6, Jan 5, 2019
+ * @since 1.3.0
+ */
+public final class SkinRenderer extends AbstractFreeMarkerRenderer {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(SkinRenderer.class);
+
+ /**
+ * HTTP request context.
+ */
+ private final RequestContext context;
+
+ /**
+ * Constructs a skin renderer with the specified request context and template name.
+ *
+ * @param context the specified request context
+ * @param templateName the specified template name
+ */
+ public SkinRenderer(final RequestContext context, final String templateName) {
+ this.context = context;
+ this.context.setRenderer(this);
+ setTemplateName(templateName);
+ }
+
+ /**
+ * Gets a template with the specified search engine bot flag and user.
+ *
+ * @param isSearchEngineBot the specified search engine bot flag
+ * @param user the specified user
+ * @return template
+ */
+ public Template getTemplate(final boolean isSearchEngineBot, final JSONObject user) {
+ String templateDirName = Sessions.getTemplateDir();
+ final String templateName = getTemplateName();
+ try {
+ Template ret;
+ try {
+ ret = Templates.getTemplate(templateDirName + "/" + templateName);
+ } catch (final Exception e) {
+ if (Symphonys.SKIN_DIR_NAME.equals(templateDirName) ||
+ Symphonys.MOBILE_SKIN_DIR_NAME.equals(templateDirName)) {
+ throw e;
+ }
+
+ // Try to load default template
+ ret = Templates.getTemplate(Symphonys.SKIN_DIR_NAME + "/" + templateName);
+ }
+
+ if (isSearchEngineBot) {
+ return ret;
+ }
+
+ ret.setLocale(Locales.getLocale());
+
+ if (null != user) {
+ ret.setTimeZone(TimeZone.getTimeZone(user.optString(UserExt.USER_TIMEZONE)));
+ } else {
+ ret.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
+ }
+
+ return ret;
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Get template [dir=" + templateDirName + ", name=" + templateName + "] error", e);
+
+ return null;
+ }
+ }
+
+ @Override
+ protected Template getTemplate() {
+ final boolean isSearchEngineBot = Sessions.isBot();
+ final JSONObject user = Sessions.getUser();
+
+ return getTemplate(isSearchEngineBot, user);
+ }
+
+ /**
+ * Processes the specified FreeMarker template with the specified request, data model, pjax hacking.
+ *
+ * @param request the specified request
+ * @param dataModel the specified data model
+ * @param template the specified FreeMarker template
+ * @return generated HTML
+ * @throws Exception exception
+ */
+ protected String genHTML(final Request request, final Map dataModel, final Template template)
+ throws Exception {
+ final boolean isPJAX = isPJAX(context);
+ dataModel.put("pjax", isPJAX);
+ if (!isPJAX) {
+ return super.genHTML(request, dataModel, template);
+ }
+
+ final StringWriter stringWriter = new StringWriter();
+ template.setOutputEncoding("UTF-8");
+ template.process(dataModel, stringWriter);
+ final long endTimeMillis = System.currentTimeMillis();
+ final String dateString = DateFormatUtils.format(endTimeMillis, "yyyy/MM/dd HH:mm:ss");
+ final long startTimeMillis = (Long) context.attr(Keys.HttpRequest.START_TIME_MILLIS);
+ final String msg = String.format("\n", endTimeMillis - startTimeMillis, dateString);
+ final String pjaxContainer = context.header("X-PJAX-Container");
+
+ return StringUtils.substringBetween(stringWriter.toString(),
+ "",
+ "") + msg;
+ }
+
+ @Override
+ protected void beforeRender(final RequestContext context) {
+ }
+
+ @Override
+ protected void afterRender(final RequestContext context) {
+ }
+
+ /**
+ * Determines whether the specified request is sending with pjax.
+ *
+ * @param context the specified request context
+ * @return {@code true} if it is sending with pjax, otherwise returns {@code false}
+ */
+ private static boolean isPJAX(final RequestContext context) {
+ final boolean pjax = Boolean.valueOf(context.header("X-PJAX"));
+ final String pjaxContainer = context.header("X-PJAX-Container");
+
+ return pjax && StringUtils.isNotBlank(pjaxContainer);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/StatisticProcessor.java b/src/main/java/org/b3log/symphony/processor/StatisticProcessor.java
new file mode 100644
index 000000000..abfc1ba5b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/StatisticProcessor.java
@@ -0,0 +1,240 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.util.Times;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.*;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data statistic processor.
+ *
+ * Shows data statistic (/statistic), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Liyuan Li
+ * @version 1.2.1.2, Dec 2, 2018
+ * @since 1.4.0
+ */
+@RequestProcessor
+public class StatisticProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(StatisticProcessor.class);
+
+ /**
+ * Month days.
+ */
+ private final List monthDays = new ArrayList<>();
+
+ /**
+ * User counts.
+ */
+ private final List userCnts = new ArrayList<>();
+
+ /**
+ * Article counts.
+ */
+ private final List articleCnts = new ArrayList<>();
+
+ /**
+ * Comment counts.
+ */
+ private final List commentCnts = new ArrayList<>();
+
+ /**
+ * History months.
+ */
+ private final List months = new ArrayList<>();
+
+ /**
+ * History user counts.
+ */
+ private final List historyUserCnts = new ArrayList<>();
+
+ /**
+ * History article counts.
+ */
+ private final List historyArticleCnts = new ArrayList<>();
+
+ /**
+ * History comment counts.
+ */
+ private final List historyCommentCnts = new ArrayList<>();
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Visit management service.
+ */
+ @Inject
+ private VisitMgmtService visitMgmtService;
+
+ /**
+ * Loads statistic data.
+ */
+ public void loadStatData() {
+ try {
+ final Date end = new Date();
+ final Date dayStart = DateUtils.addDays(end, -30);
+
+ monthDays.clear();
+ userCnts.clear();
+ articleCnts.clear();
+ commentCnts.clear();
+ months.clear();
+ historyArticleCnts.clear();
+ historyCommentCnts.clear();
+ historyUserCnts.clear();
+
+ for (int i = 0; i < 31; i++) {
+ final Date day = DateUtils.addDays(dayStart, i);
+ monthDays.add(DateFormatUtils.format(day, "yyyy-MM-dd"));
+
+ final int userCnt = userQueryService.getUserCntInDay(day);
+ userCnts.add(userCnt);
+
+ final int articleCnt = articleQueryService.getArticleCntInDay(day);
+ articleCnts.add(articleCnt);
+
+ final int commentCnt = commentQueryService.getCommentCntInDay(day);
+ commentCnts.add(commentCnt);
+ }
+
+ final JSONObject firstAdmin = userQueryService.getAdmins().get(0);
+ final long monthStartTime = Times.getMonthStartTime(firstAdmin.optLong(Keys.OBJECT_ID));
+ final Date monthStart = new Date(monthStartTime);
+
+ int i = 1;
+ while (true) {
+ final Date month = DateUtils.addMonths(monthStart, i);
+
+ if (month.after(end)) {
+ break;
+ }
+
+ i++;
+
+ months.add(DateFormatUtils.format(month, "yyyy-MM"));
+
+ final int userCnt = userQueryService.getUserCntInMonth(month);
+ historyUserCnts.add(userCnt);
+
+ final int articleCnt = articleQueryService.getArticleCntInMonth(month);
+ historyArticleCnts.add(articleCnt);
+
+ final int commentCnt = commentQueryService.getCommentCntInMonth(month);
+ historyCommentCnts.add(commentCnt);
+ }
+
+ visitMgmtService.expire();
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Loads stat data failed", e);
+ }
+ }
+
+ /**
+ * Shows data statistic.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/statistic", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showStatistic(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "statistic.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ dataModel.put("monthDays", monthDays);
+ dataModel.put("userCnts", userCnts);
+ dataModel.put("articleCnts", articleCnts);
+ dataModel.put("commentCnts", commentCnts);
+
+ dataModel.put("months", months);
+ dataModel.put("historyUserCnts", historyUserCnts);
+ dataModel.put("historyArticleCnts", historyArticleCnts);
+ dataModel.put("historyCommentCnts", historyCommentCnts);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put(Common.ONLINE_VISITOR_CNT, optionQueryService.getOnlineVisitorCount());
+ dataModel.put(Common.ONLINE_MEMBER_CNT, optionQueryService.getOnlineMemberCount());
+
+ final JSONObject statistic = optionQueryService.getStatistic();
+ dataModel.put(Option.CATEGORY_C_STATISTIC, statistic);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/TagProcessor.java b/src/main/java/org/b3log/symphony/processor/TagProcessor.java
new file mode 100644
index 000000000..f46f7c61e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/TagProcessor.java
@@ -0,0 +1,255 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.util.Paginator;
+import org.b3log.latke.util.URLs;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tag processor.
+ *
+ * Shows the tags wall (/tags), GET
+ * Show tag articles (/tag/{tagURI}), GET
+ * Query tags (/tags/query), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Liyuan Li
+ * @version 1.7.0.15, Jan 5, 2019
+ * @since 0.2.0
+ */
+@RequestProcessor
+public class TagProcessor {
+
+ /**
+ * Tag query service.
+ */
+ @Inject
+ private TagQueryService tagQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Follow query service.
+ */
+ @Inject
+ private FollowQueryService followQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Queries tags.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/tags/query", method = HttpMethod.GET)
+ public void queryTags(final RequestContext context) {
+ if (!Sessions.isLoggedIn()) {
+ context.setStatus(403);
+
+ return;
+ }
+
+ context.renderJSON().renderTrueResult();
+
+ final String titlePrefix = context.param("title");
+
+ List tags;
+ final int fetchSize = 7;
+ if (StringUtils.isBlank(titlePrefix)) {
+ tags = tagQueryService.getTags(fetchSize);
+ } else {
+ tags = tagQueryService.getTagsByPrefix(titlePrefix, fetchSize);
+ }
+
+ final List ret = new ArrayList<>();
+ for (final JSONObject tag : tags) {
+ ret.add(tag.optString(Tag.TAG_TITLE));
+ }
+
+ context.renderJSONValue(Tag.TAGS, ret);
+ }
+
+ /**
+ * Shows tags wall.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/tags", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showTagsWall(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "tags.ftl");
+ final Map dataModel = renderer.getDataModel();
+
+ final List trendTags = tagQueryService.getTrendTags(Symphonys.TAGS_CNT);
+ final List coldTags = tagQueryService.getColdTags(Symphonys.TAGS_CNT);
+
+ dataModel.put(Common.TREND_TAGS, trendTags);
+ dataModel.put(Common.COLD_TAGS, coldTags);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ }
+
+ /**
+ * Show tag articles.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/tag/{tagURI}", "/tag/{tagURI}/hot", "/tag/{tagURI}/good", "/tag/{tagURI}/reply",
+ "/tag/{tagURI}/perfect"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showTagArticles(final RequestContext context) {
+ final String tagURI = context.pathVar("tagURI");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "tag-articles.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ int pageSize = Symphonys.ARTICLE_LIST_CNT;
+
+ final JSONObject user = Sessions.getUser();
+ if (null != user) {
+ pageSize = user.optInt(UserExt.USER_LIST_PAGE_SIZE);
+
+ if (!UserExt.finshedGuide(user)) {
+ context.sendRedirect(Latkes.getServePath() + "/guide");
+
+ return;
+ }
+ }
+
+ final JSONObject tag = tagQueryService.getTagByURI(tagURI);
+ if (null == tag) {
+ context.sendError(404);
+
+ return;
+ }
+ tag.put(Common.IS_RESERVED, tagQueryService.isReservedTag(tag.optString(Tag.TAG_TITLE)));
+ dataModel.put(Tag.TAG, tag);
+ final String tagId = tag.optString(Keys.OBJECT_ID);
+ final List relatedTags = tagQueryService.getRelatedTags(tagId, Symphonys.TAG_RELATED_TAGS_CNT);
+ tag.put(Tag.TAG_T_RELATED_TAGS, (Object) relatedTags);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, tagId, Follow.FOLLOWING_TYPE_C_TAG);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ String sortModeStr = StringUtils.substringAfter(context.requestURI(), "/tag/" + tagURI);
+ int sortMode;
+ switch (sortModeStr) {
+ case "":
+ sortMode = 0;
+
+ break;
+ case "/hot":
+ sortMode = 1;
+
+ break;
+ case "/good":
+ sortMode = 2;
+
+ break;
+ case "/reply":
+ sortMode = 3;
+
+ break;
+ case "/perfect":
+ sortMode = 4;
+
+ break;
+ default:
+ sortMode = 0;
+ }
+
+ final List articles = articleQueryService.getArticlesByTag(sortMode, tag, pageNum, pageSize);
+ dataModel.put(Article.ARTICLES, articles);
+ final JSONObject tagCreator = tagQueryService.getCreator(tagId);
+ tag.put(Tag.TAG_T_CREATOR_THUMBNAIL_URL, tagCreator.optString(Tag.TAG_T_CREATOR_THUMBNAIL_URL));
+ tag.put(Tag.TAG_T_CREATOR_NAME, tagCreator.optString(Tag.TAG_T_CREATOR_NAME));
+ tag.put(Tag.TAG_T_PARTICIPANTS, (Object) tagQueryService.getParticipants(tagId, Symphonys.ARTICLE_LIST_PARTICIPANTS_CNT));
+
+ final int tagRefCnt = tag.getInt(Tag.TAG_REFERENCE_CNT);
+ final int pageCount = (int) Math.ceil(tagRefCnt / (double) pageSize);
+ final int windowSize = Symphonys.ARTICLE_LIST_WIN_SIZE;
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+
+ dataModel.put(Common.CURRENT, StringUtils.substringAfter(URLs.decode(context.requestURI()),
+ "/tag/" + tagURI));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/TopProcessor.java b/src/main/java/org/b3log/symphony/processor/TopProcessor.java
new file mode 100644
index 000000000..4302fb8bd
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/TopProcessor.java
@@ -0,0 +1,190 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Top ranking list processor.
+ *
+ * Shows top (/top), GET
+ * Top balance ranking list (/top/balance), GET
+ * Top consumption ranking list (/top/consumption), GET
+ * Top checkin ranking list (/top/checkin), GET
+ * Top link ranking list (/top/link), GET
+ *
+ *
+ * @author Liang Ding
+ * @version 1.4.0.1, Jan 5, 2019
+ * @since 1.3.0
+ */
+@RequestProcessor
+public class TopProcessor {
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Pointtransfer query service.
+ */
+ @Inject
+ private PointtransferQueryService pointtransferQueryService;
+
+ /**
+ * Activity query service.
+ */
+ @Inject
+ private ActivityQueryService activityQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Link query service.
+ */
+ @Inject
+ private LinkQueryService linkQueryService;
+
+ /**
+ * Shows top.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/top", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showTop(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "top/index.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModel.put(Common.SELECTED, Common.TOP);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Shows link ranking list.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/top/link", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showLink(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "top/link.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final List topLinks = linkQueryService.getTopLink(Symphonys.TOP_CNT);
+ dataModel.put(Common.TOP_LINKS, topLinks);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Shows balance ranking list.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/top/balance", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showBalance(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "top/balance.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final List users = pointtransferQueryService.getTopBalanceUsers(Symphonys.TOP_CNT);
+ dataModel.put(Common.TOP_BALANCE_USERS, users);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Shows consumption ranking list.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/top/consumption", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showConsumption(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "top/consumption.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final List users = pointtransferQueryService.getTopConsumptionUsers(Symphonys.TOP_CNT);
+ dataModel.put(Common.TOP_CONSUMPTION_USERS, users);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+
+ /**
+ * Shows checkin ranking list.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/top/checkin", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showCheckin(final RequestContext context) {
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "top/checkin.ftl");
+ final Map dataModel = renderer.getDataModel();
+ final List users = activityQueryService.getTopCheckinUsers(Symphonys.TOP_CNT);
+ dataModel.put(Common.TOP_CHECKIN_USERS, users);
+
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ dataModelService.fillRandomArticles(dataModel);
+ dataModelService.fillSideHotArticles(dataModel);
+ dataModelService.fillSideTags(dataModel);
+ dataModelService.fillLatestCmts(dataModel);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/UserProcessor.java b/src/main/java/org/b3log/symphony/processor/UserProcessor.java
new file mode 100644
index 000000000..42a7fb44a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/UserProcessor.java
@@ -0,0 +1,994 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.After;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.http.renderer.AbstractFreeMarkerRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.Pagination;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.CollectionUtils;
+import org.b3log.latke.util.Paginator;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.AnonymousViewCheck;
+import org.b3log.symphony.processor.advice.CSRFToken;
+import org.b3log.symphony.processor.advice.PermissionGrant;
+import org.b3log.symphony.processor.advice.UserBlockCheck;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchEndAdvice;
+import org.b3log.symphony.processor.advice.stopwatch.StopwatchStartAdvice;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.*;
+import org.json.JSONObject;
+
+import java.util.*;
+
+/**
+ * User processor.
+ *
+ * User articles (/member/{userName}), GET
+ * User anonymous articles (/member/{userName}/anonymous
+ * User comments (/member/{userName}/comments), GET
+ * User anonymous comments (/member/{userName}/comments/anonymous
+ * User following users (/member/{userName}/following/users), GET
+ * User following tags (/member/{userName}/following/tags), GET
+ * User following articles (/member/{userName}/following/articles), GET
+ * User followers (/member/{userName}/followers), GET
+ * User points (/member/{userName}/points), GET
+ * User breezemoons (/member/{userName}/breezemoons), GET
+ * List usernames (/users/names), GET
+ * List frequent emotions (/users/emotions), GET
+ *
+ *
+ * @author Liang Ding
+ * @author Zephyr
+ * @author Liyuan Li
+ * @version 1.27.1.0, Jul 7, 2019
+ * @since 0.2.0
+ */
+@RequestProcessor
+public class UserProcessor {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(UserProcessor.class);
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * Article management service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Follow query service.
+ */
+ @Inject
+ private FollowQueryService followQueryService;
+
+ /**
+ * Emotion query service.
+ */
+ @Inject
+ private EmotionQueryService emotionQueryService;
+
+ /**
+ * Emotion management service.
+ */
+ @Inject
+ private EmotionMgmtService emotionMgmtService;
+
+ /**
+ * Data model service.
+ */
+ @Inject
+ private DataModelService dataModelService;
+
+ /**
+ * Avatar query service.
+ */
+ @Inject
+ private AvatarQueryService avatarQueryService;
+
+ /**
+ * Pointtransfer query service.
+ */
+ @Inject
+ private PointtransferQueryService pointtransferQueryService;
+
+ /**
+ * Pointtransfer management service.
+ */
+ @Inject
+ private PointtransferMgmtService pointtransferMgmtService;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ /**
+ * Breezemoon query service.
+ */
+ @Inject
+ private BreezemoonQueryService breezemoonQueryService;
+
+ /**
+ * Shows user home breezemoons page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = {"/member/{userName}/breezemoons", "/member/{userName}/breezemoons/{breezemoonId}"}, method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({CSRFToken.class, PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeBreezemoons(final RequestContext context) {
+ final String breezemoonId = context.pathVar("breezemoonId");
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/breezemoons.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ JSONObject currentUser;
+ String currentUserId = null;
+ if (isLoggedIn) {
+ currentUser = Sessions.getUser();
+ currentUserId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(currentUserId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ final JSONObject result = breezemoonQueryService.getBreezemoons(currentUserId, followingId, pageNum, pageSize, windowSize);
+ List bms = (List) result.opt(Breezemoon.BREEZEMOONS);
+ dataModel.put(Common.USER_HOME_BREEZEMOONS, bms);
+
+ final JSONObject pagination = result.optJSONObject(Pagination.PAGINATION);
+ final int recordCount = pagination.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil(recordCount / (double) pageSize);
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, recordCount);
+
+ dataModel.put(Common.TYPE, Breezemoon.BREEZEMOONS);
+
+ if (StringUtils.isNotBlank(breezemoonId)) {
+ dataModel.put(Common.IS_SINGLE_BREEZEMOON_URL, true);
+ final JSONObject breezemoon = breezemoonQueryService.getBreezemoon(breezemoonId);
+ if (null == breezemoon) {
+ context.sendError(404);
+
+ return;
+ }
+
+ bms = Collections.singletonList(breezemoon);
+ breezemoonQueryService.organizeBreezemoons("admin", bms);
+ dataModel.put(Common.USER_HOME_BREEZEMOONS, bms);
+ } else {
+ dataModel.put(Common.IS_SINGLE_BREEZEMOON_URL, false);
+ }
+ }
+
+ /**
+ * Shows user home anonymous comments page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/comments/anonymous", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeAnonymousComments(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/comments.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ JSONObject currentUser = null;
+ if (isLoggedIn) {
+ currentUser = Sessions.getUser();
+ }
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ if (null == currentUser || (!currentUser.optString(Keys.OBJECT_ID).equals(user.optString(Keys.OBJECT_ID)))
+ && !Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ if (isLoggedIn) {
+ currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ final List userComments = commentQueryService.getUserComments(user.optString(Keys.OBJECT_ID), Comment.COMMENT_ANONYMOUS_C_ANONYMOUS, pageNum, pageSize, currentUser);
+ dataModel.put(Common.USER_HOME_COMMENTS, userComments);
+
+ int recordCount = 0;
+ int pageCount = 0;
+ if (!userComments.isEmpty()) {
+ final JSONObject first = userComments.get(0);
+ pageCount = first.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ recordCount = first.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ }
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, recordCount);
+
+ dataModel.put(Common.TYPE, "commentsAnonymous");
+ }
+
+ /**
+ * Shows user home anonymous articles page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/articles/anonymous", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showAnonymousArticles(final RequestContext context) {
+ final String userName = context.pathVar("userName");
+ final Request request = context.getRequest();
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/home.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ JSONObject currentUser = null;
+ if (isLoggedIn) {
+ currentUser = Sessions.getUser();
+ }
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ if (null == currentUser || (!currentUser.optString(Keys.OBJECT_ID).equals(user.optString(Keys.OBJECT_ID)))
+ && !Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
+ context.sendError(404);
+
+ return;
+ }
+
+ final int pageNum = Paginator.getPage(request);
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ if (isLoggedIn) {
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ final List userArticles = articleQueryService.getUserArticles(user.optString(Keys.OBJECT_ID), Article.ARTICLE_ANONYMOUS_C_ANONYMOUS, pageNum, pageSize);
+ dataModel.put(Common.USER_HOME_ARTICLES, userArticles);
+
+ int recordCount = 0;
+ int pageCount = 0;
+ if (!userArticles.isEmpty()) {
+ final JSONObject first = userArticles.get(0);
+ pageCount = first.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ recordCount = first.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ }
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, recordCount);
+
+ dataModel.put(Common.IS_MY_ARTICLE, userName.equals(currentUser.optString(User.USER_NAME)));
+
+ dataModel.put(Common.TYPE, "articlesAnonymous");
+ }
+
+ /**
+ * Shows user home page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHome(final RequestContext context) {
+ final String userName = context.pathVar("userName");
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+ final int pageNum = Paginator.getPage(request);
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/home.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ final List userArticles = articleQueryService.getUserArticles(user.optString(Keys.OBJECT_ID), Article.ARTICLE_ANONYMOUS_C_PUBLIC, pageNum, pageSize);
+ dataModel.put(Common.USER_HOME_ARTICLES, userArticles);
+
+ int recordCount = 0;
+ int pageCount = 0;
+ if (!userArticles.isEmpty()) {
+ final JSONObject first = userArticles.get(0);
+ pageCount = first.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ recordCount = first.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ }
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, recordCount);
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ dataModel.put(Common.IS_MY_ARTICLE, false);
+ } else {
+ dataModel.put(Common.IS_MY_ARTICLE, userName.equals(currentUser.optString(User.USER_NAME)));
+ }
+
+ dataModel.put(Common.TYPE, "home");
+ }
+
+ /**
+ * Shows user home comments page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/comments", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeComments(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/comments.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ JSONObject currentUser = null;
+ if (isLoggedIn) {
+ currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ final List userComments = commentQueryService.getUserComments(user.optString(Keys.OBJECT_ID), Comment.COMMENT_ANONYMOUS_C_PUBLIC, pageNum, pageSize, currentUser);
+ dataModel.put(Common.USER_HOME_COMMENTS, userComments);
+
+ int recordCount = 0;
+ int pageCount = 0;
+ if (!userComments.isEmpty()) {
+ final JSONObject first = userComments.get(0);
+ pageCount = first.optInt(Pagination.PAGINATION_PAGE_COUNT);
+ recordCount = first.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ }
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, recordCount);
+
+ dataModel.put(Common.TYPE, "comments");
+ }
+
+ /**
+ * Shows user home following users page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/following/users", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeFollowingUsers(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/following-users.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final JSONObject followingUsersResult = followQueryService.getFollowingUsers(followingId, pageNum, pageSize);
+ final List followingUsers = (List) followingUsersResult.opt(Keys.RESULTS);
+ dataModel.put(Common.USER_HOME_FOLLOWING_USERS, followingUsers);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+
+ for (final JSONObject followingUser : followingUsers) {
+ final String homeUserFollowingUserId = followingUser.optString(Keys.OBJECT_ID);
+
+ followingUser.put(Common.IS_FOLLOWING, followQueryService.isFollowing(followerId, homeUserFollowingUserId, Follow.FOLLOWING_TYPE_C_USER));
+ }
+ }
+
+ final int followingUserCnt = followingUsersResult.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) followingUserCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, followingUserCnt);
+
+ dataModel.put(Common.TYPE, "followingUsers");
+ }
+
+ /**
+ * Shows user home following tags page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/following/tags", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeFollowingTags(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/following-tags.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final JSONObject followingTagsResult = followQueryService.getFollowingTags(followingId, pageNum, pageSize);
+ final List followingTags = (List) followingTagsResult.opt(Keys.RESULTS);
+ dataModel.put(Common.USER_HOME_FOLLOWING_TAGS, followingTags);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+
+ for (final JSONObject followingTag : followingTags) {
+ final String homeUserFollowingTagId = followingTag.optString(Keys.OBJECT_ID);
+
+ followingTag.put(Common.IS_FOLLOWING, followQueryService.isFollowing(followerId, homeUserFollowingTagId, Follow.FOLLOWING_TYPE_C_TAG));
+ }
+ }
+
+ final int followingTagCnt = followingTagsResult.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil(followingTagCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, followingTagCnt);
+
+ dataModel.put(Common.TYPE, "followingTags");
+ }
+
+ /**
+ * Shows user home following articles page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/following/articles", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeFollowingArticles(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/following-articles.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final JSONObject followingArticlesResult = followQueryService.getFollowingArticles(followingId, pageNum, pageSize);
+ final List followingArticles = (List) followingArticlesResult.opt(Keys.RESULTS);
+ dataModel.put(Common.USER_HOME_FOLLOWING_ARTICLES, followingArticles);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+
+ for (final JSONObject followingArticle : followingArticles) {
+ final String homeUserFollowingArticleId = followingArticle.optString(Keys.OBJECT_ID);
+
+ followingArticle.put(Common.IS_FOLLOWING, followQueryService.isFollowing(followerId, homeUserFollowingArticleId, Follow.FOLLOWING_TYPE_C_ARTICLE));
+ }
+ }
+
+ final int followingArticleCnt = followingArticlesResult.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil(followingArticleCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, followingArticleCnt);
+
+ dataModel.put(Common.TYPE, "followingArticles");
+ }
+
+ /**
+ * Shows user home watching articles page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/watching/articles", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeWatchingArticles(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/watching-articles.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final JSONObject followingArticlesResult = followQueryService.getWatchingArticles(followingId, pageNum, pageSize);
+ final List followingArticles = (List) followingArticlesResult.opt(Keys.RESULTS);
+ dataModel.put(Common.USER_HOME_FOLLOWING_ARTICLES, followingArticles);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+
+ for (final JSONObject followingArticle : followingArticles) {
+ final String homeUserFollowingArticleId = followingArticle.optString(Keys.OBJECT_ID);
+
+ followingArticle.put(Common.IS_FOLLOWING, followQueryService.isFollowing(followerId, homeUserFollowingArticleId, Follow.FOLLOWING_TYPE_C_ARTICLE_WATCH));
+ }
+ }
+
+ final int followingArticleCnt = followingArticlesResult.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil(followingArticleCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, followingArticleCnt);
+
+ dataModel.put(Common.TYPE, "watchingArticles");
+ }
+
+ /**
+ * Shows user home follower users page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/followers", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomeFollowers(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/followers.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ final JSONObject followerUsersResult = followQueryService.getFollowerUsers(followingId, pageNum, pageSize);
+ final List followerUsers = (List) followerUsersResult.opt(Keys.RESULTS);
+ dataModel.put(Common.USER_HOME_FOLLOWER_USERS, followerUsers);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, followingId, Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+
+ for (final JSONObject followerUser : followerUsers) {
+ final String homeUserFollowerUserId = followerUser.optString(Keys.OBJECT_ID);
+
+ followerUser.put(Common.IS_FOLLOWING, followQueryService.isFollowing(followerId, homeUserFollowerUserId, Follow.FOLLOWING_TYPE_C_USER));
+ }
+
+ if (followerId.equals(followingId)) {
+ notificationMgmtService.makeRead(followingId, Notification.DATA_TYPE_C_NEW_FOLLOWER);
+ }
+ }
+
+ final int followerUserCnt = followerUsersResult.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) followerUserCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+ dataModel.put(Pagination.PAGINATION_RECORD_COUNT, followerUserCnt);
+
+ dataModel.put(Common.TYPE, "followers");
+
+ notificationMgmtService.makeRead(followingId, Notification.DATA_TYPE_C_NEW_FOLLOWER);
+ }
+
+ /**
+ * Shows user home points page.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/member/{userName}/points", method = HttpMethod.GET)
+ @Before({StopwatchStartAdvice.class, AnonymousViewCheck.class, UserBlockCheck.class})
+ @After({PermissionGrant.class, StopwatchEndAdvice.class})
+ public void showHomePoints(final RequestContext context) {
+ final Request request = context.getRequest();
+
+ final JSONObject user = (JSONObject) context.attr(User.USER);
+
+ final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "home/points.ftl");
+ final Map dataModel = renderer.getDataModel();
+ dataModelService.fillHeaderAndFooter(context, dataModel);
+ final int pageNum = Paginator.getPage(request);
+ final int pageSize = Symphonys.USER_HOME_LIST_CNT;
+ final int windowSize = Symphonys.USER_HOME_LIST_WIN_SIZE;
+
+ fillHomeUser(dataModel, user, roleQueryService);
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ final String followingId = user.optString(Keys.OBJECT_ID);
+ dataModel.put(Follow.FOLLOWING_ID, followingId);
+
+ final JSONObject userPointsResult
+ = pointtransferQueryService.getUserPoints(user.optString(Keys.OBJECT_ID), pageNum, pageSize);
+ final List userPoints = CollectionUtils.jsonArrayToList(userPointsResult.optJSONArray(Keys.RESULTS));
+ dataModel.put(Common.USER_HOME_POINTS, userPoints);
+
+ final boolean isLoggedIn = (Boolean) dataModel.get(Common.IS_LOGGED_IN);
+ if (isLoggedIn) {
+ final JSONObject currentUser = Sessions.getUser();
+ final String followerId = currentUser.optString(Keys.OBJECT_ID);
+
+ final boolean isFollowing = followQueryService.isFollowing(followerId, user.optString(Keys.OBJECT_ID), Follow.FOLLOWING_TYPE_C_USER);
+ dataModel.put(Common.IS_FOLLOWING, isFollowing);
+ }
+
+ final int pointsCnt = userPointsResult.optInt(Pagination.PAGINATION_RECORD_COUNT);
+ final int pageCount = (int) Math.ceil((double) pointsCnt / (double) pageSize);
+
+ final List pageNums = Paginator.paginate(pageNum, pageSize, pageCount, windowSize);
+ if (!pageNums.isEmpty()) {
+ dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
+ dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM, pageNums.get(pageNums.size() - 1));
+ }
+
+ dataModel.put(Pagination.PAGINATION_CURRENT_PAGE_NUM, pageNum);
+ dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
+ dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);
+
+ dataModel.put(Common.TYPE, "points");
+ }
+
+ /**
+ * List usernames.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/users/names", method = HttpMethod.POST)
+ public void listNames(final RequestContext context) {
+ final JSONObject result = Results.newSucc();
+ context.renderJSON(result);
+
+ final JSONObject requestJSON = context.requestJSON();
+ final String namePrefix = requestJSON.optString("name");
+ if (StringUtils.isBlank(namePrefix)) {
+ final List admins = userQueryService.getAdmins();
+ final List userNames = new ArrayList<>();
+ for (final JSONObject admin : admins) {
+ final JSONObject userName = new JSONObject();
+ userName.put(User.USER_NAME, admin.optString(User.USER_NAME));
+ final String avatar = avatarQueryService.getAvatarURLByUser(admin, "20");
+ userName.put(UserExt.USER_AVATAR_URL, avatar);
+
+ userNames.add(userName);
+ }
+
+ result.put(Common.DATA, userNames);
+
+ return;
+ }
+
+ final List userNames = userQueryService.getUserNamesByPrefix(namePrefix);
+ result.put(Common.DATA, userNames);
+ }
+
+ /**
+ * List frequent emotions.
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/users/emotions", method = HttpMethod.GET)
+ public void getFrequentEmotions(final RequestContext context) {
+ final JSONObject result = Results.newSucc();
+ context.renderJSON(result);
+
+ final List data = new ArrayList<>();
+ final JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ result.put(Common.DATA, data);
+
+ return;
+ }
+
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ String emotions = emotionQueryService.getEmojis(userId);
+ final String[] emojis = emotions.split(",");
+ for (final String emoji : emojis) {
+ String emojiChar = Emotions.toUnicode(":" + emoji + ":");
+ if (StringUtils.contains(emojiChar, ":")) {
+ final String suffix = "huaji".equals(emoji) ? ".gif" : ".png";
+ emojiChar = Latkes.getStaticServePath() + "/emoji/graphics/" + emoji + suffix;
+ }
+
+ data.add(new JSONObject().put(emoji, emojiChar));
+ }
+
+ result.put(Common.DATA, data);
+ }
+
+ /**
+ * Fills home user.
+ *
+ * @param dataModel the specified data model
+ * @param user the specified user
+ */
+ static void fillHomeUser(final Map dataModel, final JSONObject user, final RoleQueryService roleQueryService) {
+ Escapes.escapeHTML(user);
+ dataModel.put(User.USER, user);
+
+ final String roleId = user.optString(User.USER_ROLE);
+ final JSONObject role = roleQueryService.getRole(roleId);
+ user.put(Role.ROLE_NAME, role.optString(Role.ROLE_NAME));
+ user.put(UserExt.USER_T_CREATE_TIME, new Date(user.optLong(Keys.OBJECT_ID)));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/VoteProcessor.java b/src/main/java/org/b3log/symphony/processor/VoteProcessor.java
new file mode 100644
index 000000000..61ae1911e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/VoteProcessor.java
@@ -0,0 +1,317 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.HttpMethod;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.annotation.Before;
+import org.b3log.latke.http.annotation.RequestProcessing;
+import org.b3log.latke.http.annotation.RequestProcessor;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.advice.LoginCheck;
+import org.b3log.symphony.processor.advice.PermissionCheck;
+import org.b3log.symphony.service.*;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Vote processor.
+ *
+ * Votes up an article (/vote/up/article), POST
+ * Votes down an article (/vote/down/article), POST
+ * Votes up a comment (/vote/up/comment), POST
+ * Votes down a comment (/vote/down/comment), POST
+ *
+ *
+ * @author Liang Ding
+ * @version 1.3.0.6, Jun 27, 2018
+ * @since 1.3.0
+ */
+@RequestProcessor
+public class VoteProcessor {
+
+ /**
+ * Holds votes.
+ */
+ private static final Set VOTES = new HashSet<>();
+
+ /**
+ * Vote management service.
+ */
+ @Inject
+ private VoteMgmtService voteMgmtService;
+
+ /**
+ * Vote query service.
+ */
+ @Inject
+ private VoteQueryService voteQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Notification management service.
+ */
+ @Inject
+ private NotificationMgmtService notificationMgmtService;
+
+ /**
+ * Votes up a comment.
+ *
+ * The request json object:
+ *
+ * {
+ * "dataId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/vote/up/comment", method = HttpMethod.POST)
+ @Before({LoginCheck.class, PermissionCheck.class})
+ public void voteUpComment(final RequestContext context) {
+ context.renderJSON();
+
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String dataId = requestJSONObject.optString(Common.DATA_ID);
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && voteQueryService.isOwn(userId, dataId, Vote.DATA_TYPE_C_COMMENT)) {
+ context.renderFalseResult().renderMsg(langPropsService.get("cantVoteSelfLabel"));
+
+ return;
+ }
+
+ final int vote = voteQueryService.isVoted(userId, dataId);
+ if (Vote.TYPE_C_UP == vote) {
+ voteMgmtService.voteCancel(userId, dataId, Vote.DATA_TYPE_C_COMMENT);
+ } else {
+ voteMgmtService.voteUp(userId, dataId, Vote.DATA_TYPE_C_COMMENT);
+
+ final JSONObject comment = commentQueryService.getComment(dataId);
+ final String commenterId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+
+ if (!VOTES.contains(userId + dataId) && !userId.equals(commenterId)) {
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, commenterId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, dataId + "-" + userId);
+
+ notificationMgmtService.addCommentVoteUpNotification(notification);
+ }
+
+ VOTES.add(userId + dataId);
+ }
+
+ context.renderTrueResult().renderJSONValue(Vote.TYPE, vote);
+ }
+
+ /**
+ * Votes down a comment.
+ *
+ * The request json object:
+ *
+ * {
+ * "dataId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/vote/down/comment", method = HttpMethod.POST)
+ @Before({LoginCheck.class, PermissionCheck.class})
+ public void voteDownComment(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String dataId = requestJSONObject.optString(Common.DATA_ID);
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && voteQueryService.isOwn(userId, dataId, Vote.DATA_TYPE_C_COMMENT)) {
+ context.renderFalseResult().renderMsg(langPropsService.get("cantVoteSelfLabel"));
+
+ return;
+ }
+
+ final int vote = voteQueryService.isVoted(userId, dataId);
+ if (Vote.TYPE_C_DOWN == vote) {
+ voteMgmtService.voteCancel(userId, dataId, Vote.DATA_TYPE_C_COMMENT);
+ } else {
+ voteMgmtService.voteDown(userId, dataId, Vote.DATA_TYPE_C_COMMENT);
+
+ // https://github.com/b3log/symphony/issues/611
+// final JSONObject comment = commentQueryService.getComment(dataId);
+// final String commenterId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+//
+// if (!VOTES.contains(userId + dataId) && !userId.equals(commenterId)) {
+// final JSONObject notification = new JSONObject();
+// notification.put(Notification.NOTIFICATION_USER_ID, commenterId);
+// notification.put(Notification.NOTIFICATION_DATA_ID, dataId + "-" + userId);
+//
+// notificationMgmtService.addCommentVoteDownNotification(notification);
+// }
+//
+// VOTES.add(userId + dataId);
+ }
+
+ context.renderTrueResult().renderJSONValue(Vote.TYPE, vote);
+ }
+
+ /**
+ * Votes up an article.
+ *
+ * The request json object:
+ *
+ * {
+ * "dataId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/vote/up/article", method = HttpMethod.POST)
+ @Before({LoginCheck.class, PermissionCheck.class})
+ public void voteUpArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String dataId = requestJSONObject.optString(Common.DATA_ID);
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && voteQueryService.isOwn(userId, dataId, Vote.DATA_TYPE_C_ARTICLE)) {
+ context.renderFalseResult().renderMsg(langPropsService.get("cantVoteSelfLabel"));
+
+ return;
+ }
+
+ final int vote = voteQueryService.isVoted(userId, dataId);
+ if (Vote.TYPE_C_UP == vote) {
+ voteMgmtService.voteCancel(userId, dataId, Vote.DATA_TYPE_C_ARTICLE);
+ } else {
+ voteMgmtService.voteUp(userId, dataId, Vote.DATA_TYPE_C_ARTICLE);
+
+ final JSONObject article = articleQueryService.getArticle(dataId);
+ final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+
+ if (!VOTES.contains(userId + dataId) && !userId.equals(articleAuthorId)) {
+ final JSONObject notification = new JSONObject();
+ notification.put(Notification.NOTIFICATION_USER_ID, articleAuthorId);
+ notification.put(Notification.NOTIFICATION_DATA_ID, dataId + "-" + userId);
+
+ notificationMgmtService.addArticleVoteUpNotification(notification);
+ }
+
+ VOTES.add(userId + dataId);
+ }
+
+ context.renderTrueResult().renderJSONValue(Vote.TYPE, vote);
+ }
+
+ /**
+ * Votes down an article.
+ *
+ * The request json object:
+ *
+ * {
+ * "dataId": ""
+ * }
+ *
+ *
+ *
+ * @param context the specified context
+ */
+ @RequestProcessing(value = "/vote/down/article", method = HttpMethod.POST)
+ @Before({LoginCheck.class, PermissionCheck.class})
+ public void voteDownArticle(final RequestContext context) {
+ context.renderJSON();
+
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ final String dataId = requestJSONObject.optString(Common.DATA_ID);
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && voteQueryService.isOwn(userId, dataId, Vote.DATA_TYPE_C_ARTICLE)) {
+ context.renderFalseResult().renderMsg(langPropsService.get("cantVoteSelfLabel"));
+
+ return;
+ }
+
+ final int vote = voteQueryService.isVoted(userId, dataId);
+ if (Vote.TYPE_C_DOWN == vote) {
+ voteMgmtService.voteCancel(userId, dataId, Vote.DATA_TYPE_C_ARTICLE);
+ } else {
+ voteMgmtService.voteDown(userId, dataId, Vote.DATA_TYPE_C_ARTICLE);
+
+ // https://github.com/b3log/symphony/issues/611
+// final JSONObject article = articleQueryService.getArticle(dataId);
+// final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+//
+// if (!VOTES.contains(userId + dataId) && !userId.equals(articleAuthorId)) {
+// final JSONObject notification = new JSONObject();
+// notification.put(Notification.NOTIFICATION_USER_ID, articleAuthorId);
+// notification.put(Notification.NOTIFICATION_DATA_ID, dataId + "-" + userId);
+//
+// notificationMgmtService.addArticleVoteDownNotification(notification);
+// }
+//
+// VOTES.add(userId + dataId);
+ }
+
+ context.renderTrueResult().renderJSONValue(Vote.TYPE, vote);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/AnonymousViewCheck.java b/src/main/java/org/b3log/symphony/processor/advice/AnonymousViewCheck.java
new file mode 100644
index 000000000..2153ac6f1
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/AnonymousViewCheck.java
@@ -0,0 +1,208 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.Cookie;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.Response;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.RepositoryException;
+import org.b3log.latke.util.AntPathMatcher;
+import org.b3log.latke.util.URLs;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.repository.ArticleRepository;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.service.UserMgmtService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.Set;
+
+/**
+ * Anonymous view check.
+ *
+ * @author Liang Ding
+ * @version 1.3.2.1, Oct 21, 2018
+ * @since 1.6.0
+ */
+@Singleton
+public class AnonymousViewCheck extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(AnonymousViewCheck.class);
+
+ /**
+ * Article repository.
+ */
+ @Inject
+ private ArticleRepository articleRepository;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ private static Cookie getCookie(final Request request, final String name) {
+ final Set cookies = request.getCookies();
+ if (cookies.isEmpty()) {
+ return null;
+ }
+
+ for (final Cookie cookie : cookies) {
+ if (cookie.getName().equals(name)) {
+ return cookie;
+ }
+ }
+
+ return null;
+ }
+
+ private static void addCookie(final Response response, final String name, final String value) {
+ final Cookie cookie = new Cookie(name, value);
+ cookie.setPath("/");
+ cookie.setMaxAge(60 * 60 * 24); // 24 hours
+ cookie.setHttpOnly(true); // HTTP Only
+ cookie.setSecure(StringUtils.equalsIgnoreCase(Latkes.getServerScheme(), "https"));
+
+ response.addCookie(cookie);
+ }
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+ final String requestURI = context.requestURI();
+
+ final String[] skips = Symphonys.ANONYMOUS_VIEW_SKIPS.split(",");
+ for (final String skip : skips) {
+ if (AntPathMatcher.match(Latkes.getContextPath() + skip, requestURI)) {
+ return;
+ }
+ }
+
+ final JSONObject exception404 = new JSONObject();
+ exception404.put(Keys.MSG, "404, " + context.requestURI());
+ exception404.put(Keys.STATUS_CODE, 404);
+
+ final JSONObject exception401 = new JSONObject();
+ exception401.put(Keys.MSG, "401, " + context.requestURI());
+ exception401.put(Keys.STATUS_CODE, 401);
+
+ if (requestURI.startsWith(Latkes.getContextPath() + "/article/")) {
+ final String articleId = StringUtils.substringAfter(requestURI, Latkes.getContextPath() + "/article/");
+
+ try {
+ final JSONObject article = articleRepository.get(articleId);
+ if (null == article) {
+ throw new RequestProcessAdviceException(exception404);
+ }
+
+ if (Article.ARTICLE_ANONYMOUS_VIEW_C_NOT_ALLOW == article.optInt(Article.ARTICLE_ANONYMOUS_VIEW)
+ && !Sessions.isLoggedIn()) {
+ throw new RequestProcessAdviceException(exception401);
+ } else if (Article.ARTICLE_ANONYMOUS_VIEW_C_ALLOW == article.optInt(Article.ARTICLE_ANONYMOUS_VIEW)) {
+ return;
+ }
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Get article [id=" + articleId + "] failed", e);
+
+ throw new RequestProcessAdviceException(exception404);
+ }
+ }
+
+ try {
+ // Check if admin allow to anonymous view
+ final JSONObject option = optionQueryService.getOption(Option.ID_C_MISC_ALLOW_ANONYMOUS_VIEW);
+ if (!"0".equals(option.optString(Option.OPTION_VALUE))) {
+ final JSONObject currentUser = Sessions.getUser();
+
+ // https://github.com/b3log/symphony/issues/373
+ final String cookieNameVisits = "anonymous-visits";
+ final Cookie visitsCookie = getCookie(request, cookieNameVisits);
+
+ if (null == currentUser) {
+ if (null != visitsCookie) {
+ final JSONArray uris = new JSONArray(URLs.decode(visitsCookie.getValue()));
+ for (int i = 0; i < uris.length(); i++) {
+ final String uri = uris.getString(i);
+ if (uri.equals(requestURI)) {
+ return;
+ }
+ }
+
+ uris.put(requestURI);
+ if (uris.length() > Symphonys.ANONYMOUS_VIEW_URIS) {
+ throw new RequestProcessAdviceException(exception401);
+ }
+
+ addCookie(context.getResponse(), cookieNameVisits, URLs.encode(uris.toString()));
+
+ return;
+ } else {
+ final JSONArray uris = new JSONArray();
+ uris.put(requestURI);
+ addCookie(context.getResponse(), cookieNameVisits, URLs.encode(uris.toString()));
+
+ return;
+ }
+ } else { // logged in
+ if (null != visitsCookie) {
+ final Cookie cookie = new Cookie(cookieNameVisits, "");
+ cookie.setMaxAge(0);
+ cookie.setPath("/");
+
+ context.getResponse().addCookie(cookie);
+ }
+ }
+ }
+ } catch (final RequestProcessAdviceException e) {
+ throw e;
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Anonymous view check failed: " + e.getMessage());
+
+ throw new RequestProcessAdviceException(exception401);
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/CSRFCheck.java b/src/main/java/org/b3log/symphony/processor/advice/CSRFCheck.java
new file mode 100644
index 000000000..66d474ba3
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/CSRFCheck.java
@@ -0,0 +1,75 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+/**
+ * CSRF check.
+ *
+ * @author Liang Ding
+ * @version 1.0.1.0, Aug 4, 2018
+ * @since 1.3.0
+ */
+@Singleton
+public class CSRFCheck extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(CSRFCheck.class);
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.MSG, langPropsService.get("csrfCheckFailedLabel"));
+ exception.put(Keys.STATUS_CODE, false);
+
+ // 1. Check Referer
+ final String referer = context.header("Referer");
+ if (!StringUtils.startsWith(referer, StringUtils.substringBeforeLast(Latkes.getServePath(), ":"))) {
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ // 2. Check Token
+ final String clientToken = context.header(Common.CSRF_TOKEN);
+ final String serverToken = Sessions.getCSRFToken(context);
+
+ if (!StringUtils.equals(clientToken, serverToken)) {
+ throw new RequestProcessAdviceException(exception);
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/CSRFToken.java b/src/main/java/org/b3log/symphony/processor/advice/CSRFToken.java
new file mode 100644
index 000000000..0bc792584
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/CSRFToken.java
@@ -0,0 +1,54 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.renderer.AbstractResponseRenderer;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.util.Sessions;
+
+import java.util.Map;
+
+/**
+ * Fills CSRF token.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Aug 27, 2015
+ * @since 1.3.0
+ */
+@Singleton
+public class CSRFToken extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(CSRFToken.class);
+
+ @Override
+ public void doAdvice(final RequestContext context) {
+ final AbstractResponseRenderer renderer = context.getRenderer();
+ if (null != renderer) {
+ final Map dataModel = renderer.getRenderDataModel();
+
+ dataModel.put(Common.CSRF_TOKEN, Sessions.getCSRFToken(context));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/LoginCheck.java b/src/main/java/org/b3log/symphony/processor/advice/LoginCheck.java
new file mode 100644
index 000000000..cdbd69f6b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/LoginCheck.java
@@ -0,0 +1,85 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.UserMgmtService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+/**
+ * Login check. Gets user from request attribute named "user" if logged in.
+ *
+ * @author Liang Ding
+ * @version 1.2.0.3, Jun 2, 2018
+ * @since 0.2.5
+ */
+@Singleton
+public class LoginCheck extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(LoginCheck.class);
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.MSG, "401, " + context.requestURI());
+ exception.put(Keys.STATUS_CODE, 401);
+
+ JSONObject currentUser = Sessions.getUser();
+ if (null == currentUser) {
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ final int point = currentUser.optInt(UserExt.USER_POINT);
+ final int appRole = currentUser.optInt(UserExt.USER_APP_ROLE);
+ if (UserExt.USER_APP_ROLE_C_HACKER == appRole) {
+ currentUser.put(UserExt.USER_T_POINT_HEX, Integer.toHexString(point));
+ } else {
+ currentUser.put(UserExt.USER_T_POINT_CC, UserExt.toCCString(point));
+ }
+
+ request.setAttribute(User.USER, currentUser);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/PermissionCheck.java b/src/main/java/org/b3log/symphony/processor/advice/PermissionCheck.java
new file mode 100644
index 000000000..0531ff71e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/PermissionCheck.java
@@ -0,0 +1,109 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.Latkes;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.http.handler.MatchResult;
+import org.b3log.latke.http.handler.RouteHandler;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.symphony.model.Permission;
+import org.b3log.symphony.model.Role;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Set;
+
+/**
+ * Permission check.
+ *
+ * @author Liang Ding
+ * @version 1.0.1.3, Dec 19, 2018
+ * @since 1.8.0
+ */
+@Singleton
+public class PermissionCheck extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(PermissionCheck.class);
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ Stopwatchs.start("Check Permissions");
+
+ try {
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.MSG, langPropsService.get("noPermissionLabel"));
+ exception.put(Keys.STATUS_CODE, 403);
+
+ final String prefix = "permission.rule.url.";
+ final String requestURI = StringUtils.substringAfter(context.requestURI(), Latkes.getContextPath());
+ final String method = context.method();
+ String rule = prefix;
+
+ try {
+ final MatchResult matchResult = RouteHandler.doMatch(requestURI, method);
+ rule += matchResult.getMatchedUriTemplate() + "." + method;
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Match method failed", e);
+
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ final Set requisitePermissions = Symphonys.URL_PERMISSION_RULES.get(rule);
+ if (null == requisitePermissions) {
+ return;
+ }
+
+ final JSONObject user = Sessions.getUser();
+ final String roleId = null != user ? user.optString(User.USER_ROLE) : Role.ROLE_ID_C_VISITOR;
+ final Set grantPermissions = roleQueryService.getPermissions(roleId);
+
+ if (!Permission.hasPermission(requisitePermissions, grantPermissions)) {
+ throw new RequestProcessAdviceException(exception);
+ }
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/PermissionGrant.java b/src/main/java/org/b3log/symphony/processor/advice/PermissionGrant.java
new file mode 100644
index 000000000..b856f3e73
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/PermissionGrant.java
@@ -0,0 +1,88 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.renderer.AbstractResponseRenderer;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.symphony.model.Permission;
+import org.b3log.symphony.model.Role;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.json.JSONObject;
+
+import java.util.Map;
+
+/**
+ * Permission grant.
+ *
+ * @author Liang Ding
+ * @version 1.0.3.2, Jan 7, 2017
+ * @since 1.8.0
+ */
+@Singleton
+public class PermissionGrant extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(PermissionGrant.class);
+
+ /**
+ * Role query service.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ @Override
+ public void doAdvice(final RequestContext context) {
+ final AbstractResponseRenderer renderer = context.getRenderer();
+ if (null == renderer) {
+ return;
+ }
+
+ Stopwatchs.start("Grant permissions");
+ try {
+ final Map dataModel = context.getRenderer().getRenderDataModel();
+
+ final JSONObject user = Sessions.getUser();
+ final String roleId = null != user ? user.optString(User.USER_ROLE) : Role.ROLE_ID_C_VISITOR;
+ final Map permissionsGrant = roleQueryService.getPermissionsGrantMap(roleId);
+ dataModel.put(Permission.PERMISSIONS, permissionsGrant);
+
+ final JSONObject role = roleQueryService.getRole(roleId);
+
+ String noPermissionLabel = langPropsService.get("noPermissionLabel");
+ noPermissionLabel = noPermissionLabel.replace("{roleName}", role.optString(Role.ROLE_NAME));
+ dataModel.put("noPermissionLabel", noPermissionLabel);
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/UserBlockCheck.java b/src/main/java/org/b3log/symphony/processor/advice/UserBlockCheck.java
new file mode 100644
index 000000000..89f9f6a1a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/UserBlockCheck.java
@@ -0,0 +1,86 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.UserQueryService;
+import org.json.JSONObject;
+
+/**
+ * User block check. Gets user from request attribute named "user".
+ *
+ * @author Liang Ding
+ * @version 1.1.3.2, Apr 23, 2017
+ * @since 0.2.5
+ */
+@Singleton
+public class UserBlockCheck extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(UserBlockCheck.class);
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.MSG, 404);
+ exception.put(Keys.STATUS_CODE, 404);
+
+ final String userName = context.pathVar("userName");
+ if (UserExt.NULL_USER_NAME.equals(userName)) {
+ exception.put(Keys.MSG, "Nil User [" + userName + ", requestURI=" + context.requestURI() + "]");
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ final JSONObject user = userQueryService.getUserByName(userName);
+ if (null == user) {
+ exception.put(Keys.MSG, "Not found user [" + userName + ", requestURI=" + context.requestURI() + "]");
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ if (UserExt.USER_STATUS_C_NOT_VERIFIED == user.optInt(UserExt.USER_STATUS)) {
+ exception.put(Keys.MSG, "Unverified User [" + userName + ", requestURI=" + context.requestURI() + "]");
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ if (UserExt.USER_STATUS_C_INVALID == user.optInt(UserExt.USER_STATUS)) {
+ exception.put(Keys.MSG, "Blocked User [" + userName + ", requestURI=" + context.requestURI() + "]");
+ throw new RequestProcessAdviceException(exception);
+ }
+
+ request.setAttribute(User.USER, user);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchEndAdvice.java b/src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchEndAdvice.java
new file mode 100644
index 000000000..99e47aa1e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchEndAdvice.java
@@ -0,0 +1,62 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.stopwatch;
+
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.renderer.AbstractResponseRenderer;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.service.annotation.Service;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.model.Common;
+
+import java.util.Map;
+
+/**
+ * Stopwatch end advice for request processors.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Aug 2, 2015
+ * @since 0.2.0
+ */
+@Service
+public class StopwatchEndAdvice extends ProcessAdvice {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(StopwatchEndAdvice.class);
+
+ @Override
+ public void doAdvice(final RequestContext context) {
+ Stopwatchs.end();
+
+ final AbstractResponseRenderer renderer = context.getRenderer();
+ if (null != renderer) {
+ final Map dataModel = renderer.getRenderDataModel();
+ final String requestURI = context.getRequest().getRequestURI();
+
+ final long elapsed = Stopwatchs.getElapsed("Request URI [" + requestURI + "]");
+ dataModel.put(Common.ELAPSED, elapsed);
+ }
+
+ LOGGER.log(Level.TRACE, "Stopwatch: {0} {1}", Strings.LINE_SEPARATOR, Stopwatchs.getTimingStat());
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchStartAdvice.java b/src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchStartAdvice.java
new file mode 100644
index 000000000..cb5f3706a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/stopwatch/StopwatchStartAdvice.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.stopwatch;
+
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.service.annotation.Service;
+import org.b3log.latke.util.Stopwatchs;
+
+/**
+ * Stopwatch start advice for request processors.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Oct 17, 2012
+ * @since 0.2.0
+ */
+@Service
+public class StopwatchStartAdvice extends ProcessAdvice {
+
+ @Override
+ public void doAdvice(final RequestContext context) {
+ final String requestURI = context.getRequest().getRequestURI();
+ Stopwatchs.start("Request URI [" + requestURI + "]");
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001CollectValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001CollectValidation.java
new file mode 100644
index 000000000..e1f297c9e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001CollectValidation.java
@@ -0,0 +1,95 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.ActivityQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Calendar;
+
+/**
+ * Validates for activity 1A0001 collect.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.1, Jun 2, 2018
+ * @since 1.3.0
+ */
+@Singleton
+public class Activity1A0001CollectValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Activity query service.
+ */
+ @Inject
+ private ActivityQueryService activityQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ if (Symphonys.ACTIVITY_1A0001_CLOSED) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityClosedLabel")));
+ }
+
+ final Calendar calendar = Calendar.getInstance();
+
+ final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
+ if (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activity1A0001CloseLabel")));
+ }
+
+ final int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ if (hour < 16) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityCollectNotOpenLabel")));
+ }
+
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (UserExt.USER_STATUS_C_VALID != currentUser.optInt(UserExt.USER_STATUS)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("userStatusInvalidLabel")));
+ }
+
+ if (!activityQueryService.is1A0001Today(currentUser.optString(Keys.OBJECT_ID))) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityNotParticipatedLabel")));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001Validation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001Validation.java
new file mode 100644
index 000000000..54e5902e5
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/Activity1A0001Validation.java
@@ -0,0 +1,131 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.ActivityQueryService;
+import org.b3log.symphony.service.LivenessQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Calendar;
+
+/**
+ * Validates for activity 1A0001.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.4, Jun 2, 2018
+ * @since 1.3.0
+ */
+@Singleton
+public class Activity1A0001Validation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Activity query service.
+ */
+ @Inject
+ private ActivityQueryService activityQueryService;
+
+ /**
+ * Liveness query service.
+ */
+ @Inject
+ private LivenessQueryService livenessQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ final JSONObject currentUser = Sessions.getUser();
+ final String userId = currentUser.optString(Keys.OBJECT_ID);
+ final int currentLiveness = livenessQueryService.getCurrentLivenessPoint(userId);
+ final int livenessMax = Symphonys.ACTIVITY_YESTERDAY_REWARD_MAX;
+ final float liveness = (float) currentLiveness / livenessMax * 100;
+ final float livenessThreshold = Symphonys.ACTIVITY_1A0001_LIVENESS_THRESHOLD;
+ if (liveness < livenessThreshold) {
+ String msg = langPropsService.get("activityNeedLivenessLabel");
+ msg = msg.replace("${liveness}", String.valueOf(livenessThreshold) + "%");
+ msg = msg.replace("${current}", String.format("%.2f", liveness) + "%");
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, msg));
+ }
+
+ if (Symphonys.ACTIVITY_1A0001_CLOSED) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityClosedLabel")));
+ }
+
+ final Calendar calendar = Calendar.getInstance();
+
+ final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
+ if (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activity1A0001CloseLabel")));
+ }
+
+ final int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ final int minute = calendar.get(Calendar.MINUTE);
+ if (hour > 14 || (hour == 14 && minute > 55)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityEndLabel")));
+ }
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final int amount = requestJSONObject.optInt(Common.AMOUNT);
+ if (200 != amount && 300 != amount && 400 != amount && 500 != amount) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityBetFailLabel")));
+ }
+
+ final int smallOrLarge = requestJSONObject.optInt(Common.SMALL_OR_LARGE);
+ if (0 != smallOrLarge && 1 != smallOrLarge) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityBetFailLabel")));
+ }
+
+ if (UserExt.USER_STATUS_C_VALID != currentUser.optInt(UserExt.USER_STATUS)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("userStatusInvalidLabel")));
+ }
+
+ if (activityQueryService.is1A0001Today(userId)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("activityParticipatedLabel")));
+ }
+
+ final int balance = currentUser.optInt(UserExt.USER_POINT);
+ if (balance - amount < 0) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("insufficientBalanceLabel")));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/ArticleAddValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/ArticleAddValidation.java
new file mode 100644
index 000000000..f7ee4ac88
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/ArticleAddValidation.java
@@ -0,0 +1,215 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Role;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.service.TagQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.StatusCodes;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * Validates for article adding locally.
+ *
+ * @author Liang Ding
+ * @version 1.3.5.3, Nov 25, 2018
+ * @since 0.2.0
+ */
+@Singleton
+public class ArticleAddValidation extends ProcessAdvice {
+
+ /**
+ * Max article title length.
+ */
+ public static final int MAX_ARTICLE_TITLE_LENGTH = 255;
+
+ /**
+ * Max article content length.
+ */
+ public static final int MAX_ARTICLE_CONTENT_LENGTH = 102400;
+
+ /**
+ * Min article content length.
+ */
+ public static final int MIN_ARTICLE_CONTENT_LENGTH = 4;
+
+ /**
+ * Max article reward content length.
+ */
+ public static final int MAX_ARTICLE_REWARD_CONTENT_LENGTH = 102400;
+
+ /**
+ * Min article reward content length.
+ */
+ public static final int MIN_ARTICLE_REWARD_CONTENT_LENGTH = 4;
+
+ /**
+ * Validates article fields.
+ *
+ * @param context the specified HTTP request context
+ * @param requestJSONObject the specified request object
+ * @throws RequestProcessAdviceException if validate failed
+ */
+ public static void validateArticleFields(final RequestContext context, final JSONObject requestJSONObject) throws RequestProcessAdviceException {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final LangPropsService langPropsService = beanManager.getReference(LangPropsService.class);
+ final TagQueryService tagQueryService = beanManager.getReference(TagQueryService.class);
+ final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
+
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ String articleTitle = requestJSONObject.optString(Article.ARTICLE_TITLE);
+ articleTitle = StringUtils.trim(articleTitle);
+ if (StringUtils.isBlank(articleTitle) || articleTitle.length() > MAX_ARTICLE_TITLE_LENGTH) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("articleTitleErrorLabel")));
+ }
+ if (optionQueryService.containReservedWord(articleTitle)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("contentContainReservedWordLabel")));
+ }
+
+ requestJSONObject.put(Article.ARTICLE_TITLE, articleTitle);
+
+ final int articleType = requestJSONObject.optInt(Article.ARTICLE_TYPE);
+ if (Article.isInvalidArticleType(articleType)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("articleTypeErrorLabel")));
+ }
+
+ String articleTags = requestJSONObject.optString(Article.ARTICLE_TAGS);
+ articleTags = Tag.formatTags(articleTags);
+
+ if (StringUtils.isBlank(articleTags)) {
+ // 发帖时标签改为非必填 https://github.com/b3log/symphony/issues/811
+ articleTags = "待分类";
+ }
+
+ if (StringUtils.isBlank(articleTags)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("tagsEmptyErrorLabel")));
+ }
+
+ if (optionQueryService.containReservedWord(articleTags)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("contentContainReservedWordLabel")));
+ }
+
+ if (StringUtils.isNotBlank(articleTags)) {
+ String[] tagTitles = articleTags.split(",");
+
+ tagTitles = new LinkedHashSet<>(Arrays.asList(tagTitles)).toArray(new String[0]);
+ final List invalidTags = tagQueryService.getInvalidTags();
+
+ final StringBuilder tagBuilder = new StringBuilder();
+ for (int i = 0; i < tagTitles.length; i++) {
+ final String tagTitle = tagTitles[i].trim();
+
+ if (StringUtils.isBlank(tagTitle)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("tagsErrorLabel")));
+ }
+
+ if (!Tag.containsWhiteListTags(tagTitle)) {
+ if (!Tag.TAG_TITLE_PATTERN.matcher(tagTitle).matches()) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("tagsErrorLabel")));
+ }
+
+ if (tagTitle.length() > Tag.MAX_TAG_TITLE_LENGTH) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("tagsErrorLabel")));
+ }
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && ArrayUtils.contains(Symphonys.RESERVED_TAGS, tagTitle)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("articleTagReservedLabel")
+ + " [" + tagTitle + "]"));
+ }
+
+ if (invalidTags.contains(tagTitle)) {
+ continue;
+ }
+
+ tagBuilder.append(tagTitle).append(",");
+ }
+ if (tagBuilder.length() > 0) {
+ tagBuilder.deleteCharAt(tagBuilder.length() - 1);
+ }
+ requestJSONObject.put(Article.ARTICLE_TAGS, tagBuilder.toString());
+ }
+
+ String articleContent = requestJSONObject.optString(Article.ARTICLE_CONTENT);
+ articleContent = StringUtils.trim(articleContent);
+ if (StringUtils.isBlank(articleContent) || articleContent.length() > MAX_ARTICLE_CONTENT_LENGTH
+ || articleContent.length() < MIN_ARTICLE_CONTENT_LENGTH) {
+ String msg = langPropsService.get("articleContentErrorLabel");
+ msg = msg.replace("{maxArticleContentLength}", String.valueOf(MAX_ARTICLE_CONTENT_LENGTH));
+
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, msg));
+ }
+
+ if (optionQueryService.containReservedWord(articleContent)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("contentContainReservedWordLabel")));
+ }
+
+ final int rewardPoint = requestJSONObject.optInt(Article.ARTICLE_REWARD_POINT, 0);
+ if (rewardPoint < 0) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("invalidRewardPointLabel")));
+ }
+
+ final int articleQnAOfferPoint = requestJSONObject.optInt(Article.ARTICLE_QNA_OFFER_POINT, 0);
+ if (articleQnAOfferPoint < 0) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("invalidQnAOfferPointLabel")));
+ }
+
+ final String articleRewardContnt = requestJSONObject.optString(Article.ARTICLE_REWARD_CONTENT);
+ if (StringUtils.isNotBlank(articleRewardContnt) && 1 > rewardPoint) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("invalidRewardPointLabel")));
+ }
+
+ if (rewardPoint > 0) {
+ if (StringUtils.isBlank(articleRewardContnt) || articleRewardContnt.length() > MAX_ARTICLE_CONTENT_LENGTH
+ || articleRewardContnt.length() < MIN_ARTICLE_CONTENT_LENGTH) {
+ String msg = langPropsService.get("articleRewardContentErrorLabel");
+ msg = msg.replace("{maxArticleRewardContentLength}", String.valueOf(MAX_ARTICLE_REWARD_CONTENT_LENGTH));
+
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, msg));
+ }
+ }
+ }
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final JSONObject requestJSONObject = context.requestJSON();
+ validateArticleFields(context, requestJSONObject);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/ArticleUpdateValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/ArticleUpdateValidation.java
new file mode 100644
index 000000000..311d9a7c2
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/ArticleUpdateValidation.java
@@ -0,0 +1,41 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Singleton;
+import org.json.JSONObject;
+
+/**
+ * Validates for article updating.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.2, Nov 10, 2018
+ * @since 0.2.0
+ */
+@Singleton
+public class ArticleUpdateValidation extends ProcessAdvice {
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final JSONObject requestJSONObject = context.requestJSON();
+ ArticleAddValidation.validateArticleFields(context, requestJSONObject);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/ChatMsgAddValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/ChatMsgAddValidation.java
new file mode 100644
index 000000000..fc4cd558c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/ChatMsgAddValidation.java
@@ -0,0 +1,97 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Role;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+/**
+ * Validates for chat message adding.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.2, Jun 2, 2018
+ * @since 1.4.0
+ */
+@Singleton
+public class ChatMsgAddValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (System.currentTimeMillis() - currentUser.optLong(UserExt.USER_LATEST_CMT_TIME) < Symphonys.MIN_STEP_CHAT_TIME
+ && !Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))) {
+ throw new Exception(langPropsService.get("tooFrequentCmtLabel"));
+ }
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ String content = requestJSONObject.optString(Common.CONTENT);
+ content = StringUtils.trim(content);
+ if (StringUtils.isBlank(content) || content.length() > 4096) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("commentErrorLabel")));
+ }
+
+ if (optionQueryService.containReservedWord(content)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("contentContainReservedWordLabel")));
+ }
+
+ requestJSONObject.put(Common.CONTENT, content);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/CommentAddValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/CommentAddValidation.java
new file mode 100644
index 000000000..02696033c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/CommentAddValidation.java
@@ -0,0 +1,102 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Comment;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.service.CommentQueryService;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.util.StatusCodes;
+import org.json.JSONObject;
+
+/**
+ * Validates for comment adding locally.
+ *
+ * @author Liang Ding
+ * @version 1.3.0.4, Apr 7, 2019
+ * @since 0.2.0
+ */
+@Singleton
+public class CommentAddValidation extends ProcessAdvice {
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+ final JSONObject requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ validateCommentFields(requestJSONObject);
+ }
+
+ /**
+ * Validates comment fields.
+ *
+ * @param requestJSONObject the specified request object
+ * @throws RequestProcessAdviceException if validate failed
+ */
+ public static void validateCommentFields(final JSONObject requestJSONObject) throws RequestProcessAdviceException {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final LangPropsService langPropsService = beanManager.getReference(LangPropsService.class);
+ final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
+ final ArticleQueryService articleQueryService = beanManager.getReference(ArticleQueryService.class);
+ final CommentQueryService commentQueryService = beanManager.getReference(CommentQueryService.class);
+
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ final String commentContent = StringUtils.trim(requestJSONObject.optString(Comment.COMMENT_CONTENT));
+ if (StringUtils.isBlank(commentContent) || commentContent.length() > Comment.MAX_COMMENT_CONTENT_LENGTH) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("commentErrorLabel")));
+ }
+
+ if (optionQueryService.containReservedWord(commentContent)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("contentContainReservedWordLabel")));
+ }
+
+ final String articleId = requestJSONObject.optString(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(articleId)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("commentArticleErrorLabel")));
+ }
+
+ final JSONObject article = articleQueryService.getArticleById(articleId);
+ if (null == article) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("commentArticleErrorLabel")));
+ }
+
+ if (!article.optBoolean(Article.ARTICLE_COMMENTABLE)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("notAllowCmtLabel")));
+ }
+
+ final String originalCommentId = requestJSONObject.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID);
+ if (StringUtils.isNotBlank(originalCommentId)) {
+ final JSONObject originalCmt = commentQueryService.getComment(originalCommentId);
+ if (null == originalCmt) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("commentArticleErrorLabel")));
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/CommentUpdateValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/CommentUpdateValidation.java
new file mode 100644
index 000000000..e80b3bd9d
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/CommentUpdateValidation.java
@@ -0,0 +1,110 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Comment;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.b3log.symphony.service.CommentQueryService;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.util.StatusCodes;
+import org.json.JSONObject;
+
+/**
+ * Validates for comment updating locally.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, J, 2019
+ * @since 2.1.0
+ */
+@Singleton
+public class CommentUpdateValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Comment query service.
+ */
+ @Inject
+ private CommentQueryService commentQueryService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Validates comment fields.
+ *
+ * @param requestJSONObject the specified request object
+ * @throws RequestProcessAdviceException if validate failed
+ */
+ private static void validateCommentFields(final JSONObject requestJSONObject) throws RequestProcessAdviceException {
+ final BeanManager beanManager = BeanManager.getInstance();
+ final LangPropsService langPropsService = beanManager.getReference(LangPropsService.class);
+ final OptionQueryService optionQueryService = beanManager.getReference(OptionQueryService.class);
+
+ final JSONObject exception = new JSONObject();
+ exception.put(Keys.STATUS_CODE, StatusCodes.ERR);
+
+ final String commentContent = StringUtils.trim(requestJSONObject.optString(Comment.COMMENT_CONTENT));
+ if (StringUtils.isBlank(commentContent) || commentContent.length() > Comment.MAX_COMMENT_CONTENT_LENGTH) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("commentErrorLabel")));
+ }
+
+ if (optionQueryService.containReservedWord(commentContent)) {
+ throw new RequestProcessAdviceException(exception.put(Keys.MSG, langPropsService.get("contentContainReservedWordLabel")));
+ }
+ }
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()).
+ put(Keys.STATUS_CODE, StatusCodes.ERR));
+ }
+
+ validateCommentFields(requestJSONObject);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/PointTransferValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/PointTransferValidation.java
new file mode 100644
index 000000000..a0efa8e63
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/PointTransferValidation.java
@@ -0,0 +1,118 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Pointtransfer;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+import org.jsoup.Jsoup;
+import org.jsoup.safety.Whitelist;
+
+/**
+ * Validates for user point transfer.
+ *
+ * @author Liang Ding
+ * @version 1.0.1.4, Oct 1, 2018
+ * @since 1.3.0
+ */
+@Singleton
+public class PointTransferValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final String userName = requestJSONObject.optString(User.USER_NAME);
+ if (StringUtils.isBlank(userName)
+ || UserExt.COM_BOT_NAME.equals(userName) || UserExt.NULL_USER_NAME.equals(userName)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("notFoundUserLabel")));
+ }
+
+ final int amount = requestJSONObject.optInt(Common.AMOUNT);
+ if (amount < 1 || amount > 5000) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("amountInvalidLabel")));
+ }
+
+ JSONObject toUser = userQueryService.getUserByName(userName);
+ if (null == toUser) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("notFoundUserLabel")));
+ }
+
+ if (UserExt.USER_STATUS_C_VALID != toUser.optInt(UserExt.USER_STATUS)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("userStatusInvalidLabel")));
+ }
+
+ request.setAttribute(Common.TO_USER, toUser);
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (UserExt.USER_STATUS_C_VALID != currentUser.optInt(UserExt.USER_STATUS)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("userStatusInvalidLabel")));
+ }
+
+ if (currentUser.optString(User.USER_NAME).equals(toUser.optString(User.USER_NAME))) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("cannotTransferSelfLabel")));
+ }
+
+ final int balanceMinLimit = Symphonys.POINT_TRANSER_MIN;
+ final int balance = currentUser.optInt(UserExt.USER_POINT);
+ if (balance - amount < balanceMinLimit) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("insufficientBalanceLabel")));
+ }
+
+ String memo = StringUtils.trim(requestJSONObject.optString(Pointtransfer.MEMO));
+ if (128 < StringUtils.length(memo)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("memoTooLargeLabel")));
+ }
+ memo = Jsoup.clean(memo, Whitelist.none());
+ request.setAttribute(Pointtransfer.MEMO, memo);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/ShowArticleUpdateValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/ShowArticleUpdateValidation.java
new file mode 100644
index 000000000..b91463bbd
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/ShowArticleUpdateValidation.java
@@ -0,0 +1,76 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.service.ArticleQueryService;
+import org.json.JSONObject;
+
+/**
+ * Validates for show article update.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Mar 11, 2013
+ * @since 0.2.0
+ */
+@Singleton
+public class ShowArticleUpdateValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Article query service.
+ */
+ @Inject
+ private ArticleQueryService articleQueryService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject article;
+ try {
+ final String articleId = context.param("id");
+ if (StringUtils.isBlank(articleId)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("updateArticleNotFoundLabel")));
+ }
+
+ article = articleQueryService.getArticleById(articleId);
+ if (null == article) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("updateArticleNotFoundLabel")));
+ }
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ request.setAttribute(Article.ARTICLE, article);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/UpdatePasswordValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/UpdatePasswordValidation.java
new file mode 100644
index 000000000..c88be66ef
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/UpdatePasswordValidation.java
@@ -0,0 +1,70 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.json.JSONObject;
+
+/**
+ * Validates for user password update.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Oct 29, 2012
+ * @since 0.2.0
+ */
+@Singleton
+public class UpdatePasswordValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final String oldPwd = requestJSONObject.optString(User.USER_PASSWORD);
+ if (StringUtils.isBlank(oldPwd)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("emptyOldPwdLabel")));
+ }
+
+ final String pwd = requestJSONObject.optString(User.USER_PASSWORD);
+ if (UserRegisterValidation.invalidUserPassword(pwd)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("invalidPasswordLabel")));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/UpdateProfilesValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/UpdateProfilesValidation.java
new file mode 100644
index 000000000..978177778
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/UpdateProfilesValidation.java
@@ -0,0 +1,184 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.model.Role;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.util.Sessions;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+
+/**
+ * Validates for user profiles update.
+ *
+ * @author Liang Ding
+ * @version 2.2.2.5, Sep 6, 2016
+ * @since 0.2.0
+ */
+@Singleton
+public class UpdateProfilesValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Max user nickname length.
+ */
+ public static final int MAX_USER_NICKNAME_LENGTH = 20;
+
+ /**
+ * Max user URL length.
+ */
+ public static final int MAX_USER_URL_LENGTH = 100;
+
+ /**
+ * Max user QQ length.
+ */
+ public static final int MAX_USER_QQ_LENGTH = 12;
+
+ /**
+ * Max user intro length.
+ */
+ public static final int MAX_USER_INTRO_LENGTH = 255;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final String userURL = requestJSONObject.optString(User.USER_URL);
+ if (StringUtils.isNotBlank(userURL) && invalidUserURL(userURL)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG,
+ "URL" + langPropsService.get("colonLabel") + langPropsService.get("invalidUserURLLabel")));
+ }
+
+ final String userQQ = requestJSONObject.optString(UserExt.USER_QQ);
+ if (StringUtils.isNotBlank(userQQ) && (!Strings.isNumeric(userQQ) || userQQ.length() > MAX_USER_QQ_LENGTH)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG,
+ langPropsService.get("invalidUserQQLabel")));
+ }
+
+ final String userNickname = requestJSONObject.optString(UserExt.USER_NICKNAME);
+ if (StringUtils.isNotBlank(userNickname) && userNickname.length() > MAX_USER_NICKNAME_LENGTH) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("invalidUserNicknameLabel")));
+ }
+
+ final String userIntro = requestJSONObject.optString(UserExt.USER_INTRO);
+ if (StringUtils.isNotBlank(userIntro) && userIntro.length() > MAX_USER_INTRO_LENGTH) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("invalidUserIntroLabel")));
+ }
+
+ final int userCommentViewMode = requestJSONObject.optInt(UserExt.USER_COMMENT_VIEW_MODE);
+ if (userCommentViewMode != UserExt.USER_COMMENT_VIEW_MODE_C_REALTIME
+ && userCommentViewMode != UserExt.USER_COMMENT_VIEW_MODE_C_TRADITIONAL) {
+ requestJSONObject.put(UserExt.USER_COMMENT_VIEW_MODE, UserExt.USER_COMMENT_VIEW_MODE_C_TRADITIONAL);
+ }
+
+ final String tagErrMsg = langPropsService.get("selfTagLabel") + langPropsService.get("colonLabel")
+ + langPropsService.get("tagsErrorLabel");
+
+ String userTags = requestJSONObject.optString(UserExt.USER_TAGS);
+ if (StringUtils.isNotBlank(userTags)) {
+ userTags = Tag.formatTags(userTags);
+ String[] tagTitles = userTags.split(",");
+ if (null == tagTitles || 0 == tagTitles.length) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, tagErrMsg));
+ }
+
+ tagTitles = new LinkedHashSet<>(Arrays.asList(tagTitles)).toArray(new String[0]);
+
+ final StringBuilder tagBuilder = new StringBuilder();
+ for (int i = 0; i < tagTitles.length; i++) {
+ final String tagTitle = tagTitles[i].trim();
+
+ if (StringUtils.isBlank(tagTitle)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, tagErrMsg));
+ }
+
+ if (Tag.containsWhiteListTags(tagTitle)) {
+ tagBuilder.append(tagTitle).append(",");
+
+ continue;
+ }
+
+ if (!Tag.TAG_TITLE_PATTERN.matcher(tagTitle).matches()) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, tagErrMsg));
+ }
+
+ if (tagTitle.length() > Tag.MAX_TAG_TITLE_LENGTH) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, tagErrMsg));
+ }
+
+ final JSONObject currentUser = Sessions.getUser();
+ if (!Role.ROLE_ID_C_ADMIN.equals(currentUser.optString(User.USER_ROLE))
+ && ArrayUtils.contains(Symphonys.RESERVED_TAGS, tagTitle)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG,
+ langPropsService.get("selfTagLabel") + langPropsService.get("colonLabel")
+ + langPropsService.get("articleTagReservedLabel") + " [" + tagTitle + "]"));
+ }
+
+ tagBuilder.append(tagTitle).append(",");
+ }
+ if (tagBuilder.length() > 0) {
+ tagBuilder.deleteCharAt(tagBuilder.length() - 1);
+ }
+
+ requestJSONObject.put(UserExt.USER_TAGS, tagBuilder.toString());
+ }
+ }
+
+ /**
+ * Checks whether the specified user URL is invalid.
+ *
+ * @param userURL the specified user URL
+ * @return {@code true} if it is invalid, returns {@code false} otherwise
+ */
+ private boolean invalidUserURL(final String userURL) {
+ if (!Strings.isURL(userURL)) {
+ return true;
+ }
+
+ return userURL.length() > MAX_USER_URL_LENGTH;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/UserForgetPwdValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/UserForgetPwdValidation.java
new file mode 100644
index 000000000..00909749f
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/UserForgetPwdValidation.java
@@ -0,0 +1,83 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.processor.CaptchaProcessor;
+import org.json.JSONObject;
+
+/**
+ * User forget password form validation.
+ *
+ * @author Liang Ding
+ * @version 1.0.1.0, Mar 10, 2016
+ * @since 1.4.0
+ */
+@Singleton
+public class UserForgetPwdValidation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final String email = requestJSONObject.optString(User.USER_EMAIL);
+ final String captcha = requestJSONObject.optString(CaptchaProcessor.CAPTCHA);
+
+ checkField(CaptchaProcessor.invalidCaptcha(captcha), "submitFailedLabel", "captchaErrorLabel");
+ checkField(!Strings.isEmail(email), "submitFailedLabel", "invalidEmailLabel");
+ }
+
+ /**
+ * Checks field.
+ *
+ * @param invalid the specified invalid flag
+ * @param failLabel the specified fail label
+ * @param fieldLabel the specified field label
+ * @throws RequestProcessAdviceException request process advice exception
+ */
+ private void checkField(final boolean invalid, final String failLabel, final String fieldLabel)
+ throws RequestProcessAdviceException {
+ if (invalid) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get(failLabel)
+ + " - " + langPropsService.get(fieldLabel)));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/UserRegister2Validation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/UserRegister2Validation.java
new file mode 100644
index 000000000..30dd91178
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/UserRegister2Validation.java
@@ -0,0 +1,120 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.symphony.model.Option;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.OptionQueryService;
+import org.json.JSONObject;
+
+/**
+ * UserRegister2Validation for validate {@link org.b3log.symphony.processor.LoginProcessor} register2(Type POST) method.
+ *
+ * @author Liang Ding
+ * @version 1.1.1.0, Jul 3, 2016
+ * @since 1.3.0
+ */
+@Singleton
+public class UserRegister2Validation extends ProcessAdvice {
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+
+ /**
+ * Max password length.
+ *
+ *
+ * MD5 32
+ *
+ */
+ private static final int MAX_PWD_LENGTH = 32;
+
+ /**
+ * Min password length.
+ */
+ private static final int MIN_PWD_LENGTH = 1;
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+
+ // check if admin allow to register
+ final JSONObject option = optionQueryService.getOption(Option.ID_C_MISC_ALLOW_REGISTER);
+ if ("1".equals(option.optString(Option.OPTION_VALUE))) {
+ throw new Exception(langPropsService.get("notAllowRegisterLabel"));
+ }
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final int appRole = requestJSONObject.optInt(UserExt.USER_APP_ROLE);
+ final String password = requestJSONObject.optString(User.USER_PASSWORD);
+ checkField(UserExt.USER_APP_ROLE_C_HACKER != appRole
+ && UserExt.USER_APP_ROLE_C_PAINTER != appRole, "registerFailLabel", "invalidAppRoleLabel");
+ checkField(invalidUserPassword(password), "registerFailLabel", "invalidPasswordLabel");
+ }
+
+ /**
+ * Checks password, length [1, 16].
+ *
+ * @param password the specific password
+ * @return {@code true} if it is invalid, returns {@code false} otherwise
+ */
+ public static boolean invalidUserPassword(final String password) {
+ return password.length() < MIN_PWD_LENGTH || password.length() > MAX_PWD_LENGTH;
+ }
+
+ /**
+ * Checks field.
+ *
+ * @param invalid the specified invalid flag
+ * @param failLabel the specified fail label
+ * @param fieldLabel the specified field label
+ * @throws RequestProcessAdviceException request process advice exception
+ */
+ private void checkField(final boolean invalid, final String failLabel, final String fieldLabel)
+ throws RequestProcessAdviceException {
+ if (invalid) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get(failLabel)
+ + " - " + langPropsService.get(fieldLabel)));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/advice/validate/UserRegisterValidation.java b/src/main/java/org/b3log/symphony/processor/advice/validate/UserRegisterValidation.java
new file mode 100644
index 000000000..2e47b701a
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/advice/validate/UserRegisterValidation.java
@@ -0,0 +1,256 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.advice.validate;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Request;
+import org.b3log.latke.http.RequestContext;
+import org.b3log.latke.http.advice.ProcessAdvice;
+import org.b3log.latke.http.advice.RequestProcessAdviceException;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Strings;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.processor.CaptchaProcessor;
+import org.b3log.symphony.service.InvitecodeQueryService;
+import org.b3log.symphony.service.OptionQueryService;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.json.JSONObject;
+
+import java.util.Map;
+
+/**
+ * UserRegisterValidation for validate {@link org.b3log.symphony.processor.LoginProcessor} register(Type POST) method.
+ *
+ * @author Love Yao
+ * @author Liang Ding
+ * @version 1.5.2.13, Mar 23, 2019
+ * @since 0.2.0
+ */
+@Singleton
+public class UserRegisterValidation extends ProcessAdvice {
+
+ /**
+ * Max user name length.
+ */
+ public static final int MAX_USER_NAME_LENGTH = 64;
+ /**
+ * Min user name length.
+ */
+ public static final int MIN_USER_NAME_LENGTH = 1;
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(UserRegisterValidation.class);
+ /**
+ * Max password length.
+ *
+ * MD5 32
+ *
+ */
+ private static final int MAX_PWD_LENGTH = 32;
+ /**
+ * Min password length.
+ */
+ private static final int MIN_PWD_LENGTH = 1;
+ /**
+ * Captcha length.
+ */
+ private static final int CAPTCHA_LENGTH = 4;
+ /**
+ * Invitecode length.
+ */
+ private static final int INVITECODE_LENGHT = 16;
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+ /**
+ * Option query service.
+ */
+ @Inject
+ private OptionQueryService optionQueryService;
+ /**
+ * Invitecode query service.
+ */
+ @Inject
+ private InvitecodeQueryService invitecodeQueryService;
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+ /**
+ * Role query servicce.
+ */
+ @Inject
+ private RoleQueryService roleQueryService;
+
+ /**
+ * Checks whether the specified name is invalid.
+ *
+ * A valid user name:
+ *
+ * length [1, 64]
+ * content {a-z, A-Z, 0-9, -}
+ *
+ *
+ *
+ * @param name the specified name
+ * @return {@code true} if it is invalid, returns {@code false} otherwise
+ */
+ public static boolean invalidUserName(final String name) {
+ if (StringUtils.isBlank(name)) {
+ return true;
+ }
+
+ if (UserExt.isReservedUserName(name)) {
+ return true;
+ }
+
+ final int length = name.length();
+ if (length < MIN_USER_NAME_LENGTH || length > MAX_USER_NAME_LENGTH) {
+ return true;
+ }
+
+ char c;
+ for (int i = 0; i < length; i++) {
+ c = name.charAt(i);
+ if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || '-' == c) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks password, length [1, 16].
+ *
+ * @param password the specific password
+ * @return {@code true} if it is invalid, returns {@code false} otherwise
+ */
+ public static boolean invalidUserPassword(final String password) {
+ return password.length() < MIN_PWD_LENGTH || password.length() > MAX_PWD_LENGTH;
+ }
+
+ @Override
+ public void doAdvice(final RequestContext context) throws RequestProcessAdviceException {
+ final Request request = context.getRequest();
+
+ JSONObject requestJSONObject;
+ try {
+ requestJSONObject = context.requestJSON();
+ request.setAttribute(Keys.REQUEST, requestJSONObject);
+ } catch (final Exception e) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, e.getMessage()));
+ }
+
+ final String referral = requestJSONObject.optString(Common.REFERRAL);
+
+ // check if admin allow to register
+ final JSONObject option = optionQueryService.getOption(Option.ID_C_MISC_ALLOW_REGISTER);
+ if ("1".equals(option.optString(Option.OPTION_VALUE))) {
+ checkField(true, "registerFailLabel", "notAllowRegisterLabel");
+ }
+
+ boolean useInvitationLink = false;
+
+ if (!UserRegisterValidation.invalidUserName(referral)) {
+ try {
+ final JSONObject referralUser = userQueryService.getUserByName(referral);
+ if (null != referralUser) {
+
+ final Map permissions =
+ roleQueryService.getUserPermissionsGrantMap(referralUser.optString(Keys.OBJECT_ID));
+ final JSONObject useILPermission =
+ permissions.get(Permission.PERMISSION_ID_C_COMMON_USE_INVITATION_LINK);
+ useInvitationLink = useILPermission.optBoolean(Permission.PERMISSION_T_GRANT);
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.WARN, "Query user [name=" + referral + "] failed", e);
+ }
+ }
+
+ // invitecode register
+ if (!useInvitationLink && "2".equals(option.optString(Option.OPTION_VALUE))) {
+ final String invitecode = requestJSONObject.optString(Invitecode.INVITECODE);
+
+ if (StringUtils.isBlank(invitecode) || INVITECODE_LENGHT != invitecode.length()) {
+ checkField(true, "registerFailLabel", "invalidInvitecodeLabel");
+ }
+
+ final JSONObject ic = invitecodeQueryService.getInvitecode(invitecode);
+ if (null == ic) {
+ checkField(true, "registerFailLabel", "invalidInvitecodeLabel");
+ }
+
+ if (Invitecode.STATUS_C_UNUSED != ic.optInt(Invitecode.STATUS)) {
+ checkField(true, "registerFailLabel", "usedInvitecodeLabel");
+ }
+ }
+
+ // open register
+ if (useInvitationLink || "0".equals(option.optString(Option.OPTION_VALUE))) {
+ final String captcha = requestJSONObject.optString(CaptchaProcessor.CAPTCHA);
+ checkField(CaptchaProcessor.invalidCaptcha(captcha), "registerFailLabel", "captchaErrorLabel");
+ }
+
+ final String name = requestJSONObject.optString(User.USER_NAME);
+ final String email = requestJSONObject.optString(User.USER_EMAIL);
+ final int appRole = requestJSONObject.optInt(UserExt.USER_APP_ROLE);
+ //final String password = requestJSONObject.optString(User.USER_PASSWORD);
+
+ if (UserExt.isReservedUserName(name)) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get("registerFailLabel")
+ + " - " + langPropsService.get("reservedUserNameLabel")));
+ }
+
+ checkField(invalidUserName(name), "registerFailLabel", "invalidUserNameLabel");
+ checkField(!Strings.isEmail(email), "registerFailLabel", "invalidEmailLabel");
+ checkField(!UserExt.isValidMailDomain(email), "registerFailLabel", "invalidEmail1Label");
+ checkField(UserExt.USER_APP_ROLE_C_HACKER != appRole
+ && UserExt.USER_APP_ROLE_C_PAINTER != appRole, "registerFailLabel", "invalidAppRoleLabel");
+ //checkField(invalidUserPassword(password), "registerFailLabel", "invalidPasswordLabel");
+ }
+
+ /**
+ * Checks field.
+ *
+ * @param invalid the specified invalid flag
+ * @param failLabel the specified fail label
+ * @param fieldLabel the specified field label
+ * @throws RequestProcessAdviceException request process advice exception
+ */
+ private void checkField(final boolean invalid, final String failLabel, final String fieldLabel)
+ throws RequestProcessAdviceException {
+ if (invalid) {
+ throw new RequestProcessAdviceException(new JSONObject().put(Keys.MSG, langPropsService.get(failLabel)
+ + " - " + langPropsService.get(fieldLabel)));
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/channel/ArticleChannel.java b/src/main/java/org/b3log/symphony/processor/channel/ArticleChannel.java
new file mode 100644
index 000000000..1a789431f
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/channel/ArticleChannel.java
@@ -0,0 +1,295 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.channel;
+
+import freemarker.template.Template;
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Session;
+import org.b3log.latke.http.WebSocketChannel;
+import org.b3log.latke.http.WebSocketSession;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.util.Locales;
+import org.b3log.symphony.model.*;
+import org.b3log.symphony.service.RoleQueryService;
+import org.b3log.symphony.service.UserQueryService;
+import org.b3log.symphony.util.Templates;
+import org.json.JSONObject;
+
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Article channel.
+ *
+ * @author Liang Ding
+ * @version 2.0.0.0, Nov 6, 2019
+ * @since 1.3.0
+ */
+@Singleton
+public class ArticleChannel implements WebSocketChannel {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleChannel.class);
+
+ /**
+ * Session set.
+ */
+ public static final Set SESSIONS = Collections.newSetFromMap(new ConcurrentHashMap());
+
+ /**
+ * Article viewing map <articleId, count>.
+ */
+ public static final Map ARTICLE_VIEWS = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Notifies the specified article heat message to browsers.
+ *
+ * @param message the specified message, for example,
+ * "articleId": "",
+ * "operation": "" // "+"/"-"
+ */
+ public static void notifyHeat(final JSONObject message) {
+ message.put(Common.TYPE, Article.ARTICLE_T_HEAT);
+
+ final String msgStr = message.toString();
+
+ for (final WebSocketSession session : SESSIONS) {
+ final String viewingArticleId = session.getParameter(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(viewingArticleId)
+ || !viewingArticleId.equals(message.optString(Article.ARTICLE_T_ID))) {
+ continue;
+ }
+
+ session.sendText(msgStr);
+ }
+ }
+
+ /**
+ * Notifies the specified comment message to browsers.
+ *
+ * @param message the specified message
+ */
+ public static void notifyComment(final JSONObject message) {
+ message.put(Common.TYPE, Comment.COMMENT);
+
+ final BeanManager beanManager = BeanManager.getInstance();
+ final UserQueryService userQueryService = beanManager.getReference(UserQueryService.class);
+ final RoleQueryService roleQueryService = beanManager.getReference(RoleQueryService.class);
+ final LangPropsService langPropsService = beanManager.getReference(LangPropsService.class);
+ final JSONObject article = message.optJSONObject(Article.ARTICLE);
+
+ for (final WebSocketSession session : SESSIONS) {
+ final String viewingArticleId = session.getParameter(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(viewingArticleId)
+ || !viewingArticleId.equals(message.optString(Article.ARTICLE_T_ID))) {
+ continue;
+ }
+
+ final int articleType = Integer.valueOf(session.getParameter(Article.ARTICLE_TYPE));
+ final Session httpSession = session.getHttpSession();
+ final String userStr = httpSession.getAttribute(User.USER);
+ final boolean isLoggedIn = null != userStr;
+ JSONObject user = null;
+ if (isLoggedIn) {
+ user = new JSONObject(userStr);
+ }
+
+ try {
+ if (Article.ARTICLE_TYPE_C_DISCUSSION == articleType) {
+ if (!isLoggedIn) {
+ continue;
+ }
+
+ final String userName = user.optString(User.USER_NAME);
+ final String userRole = user.optString(User.USER_ROLE);
+
+ final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID);
+ final String userId = user.optString(Keys.OBJECT_ID);
+ if (!userId.equals(articleAuthorId)) {
+ final String articleContent = article.optString(Article.ARTICLE_CONTENT);
+ final Set userNames = userQueryService.getUserNames(articleContent);
+
+ boolean invited = false;
+ for (final String inviteUserName : userNames) {
+ if (inviteUserName.equals(userName)) {
+ invited = true;
+
+ break;
+ }
+ }
+
+ if (Role.ROLE_ID_C_ADMIN.equals(userRole)) {
+ invited = true;
+ }
+
+ if (!invited) {
+ continue; // next session
+ }
+ }
+ }
+
+ message.put(Comment.COMMENT_T_NICE, false);
+ message.put(Common.REWARED_COUNT, 0);
+ message.put(Comment.COMMENT_T_VOTE, -1);
+ message.put(Common.REWARDED, false);
+ message.put(Comment.COMMENT_REVISION_COUNT, 1);
+
+ final Map dataModel = new HashMap();
+ dataModel.put(Common.IS_LOGGED_IN, isLoggedIn);
+ dataModel.put(Common.CURRENT_USER, user);
+ article.put(Common.OFFERED, false);
+ dataModel.put(Article.ARTICLE, article);
+ dataModel.put(Common.CSRF_TOKEN, httpSession.getAttribute(Common.CSRF_TOKEN));
+ Keys.fillServer(dataModel);
+ dataModel.put(Comment.COMMENT, message);
+
+ if (isLoggedIn) {
+ dataModel.putAll(langPropsService.getAll(Locales.getLocale(user.optString(UserExt.USER_LANGUAGE))));
+ final String userId = user.optString(Keys.OBJECT_ID);
+ final Map permissions
+ = roleQueryService.getUserPermissionsGrantMap(userId);
+ dataModel.put(Permission.PERMISSIONS, permissions);
+ } else {
+ dataModel.putAll(langPropsService.getAll(Locales.getLocale()));
+ final Map permissions
+ = roleQueryService.getPermissionsGrantMap(Role.ROLE_ID_C_VISITOR);
+ dataModel.put(Permission.PERMISSIONS, permissions);
+ }
+
+ final String templateDirName = httpSession.getAttribute(Keys.TEMAPLTE_DIR_NAME);
+ final Template template = Templates.getTemplate(templateDirName + "/common/comment.ftl");
+ final StringWriter stringWriter = new StringWriter();
+ template.process(dataModel, stringWriter);
+ stringWriter.close();
+
+ message.put("cmtTpl", stringWriter.toString());
+ final String msgStr = message.toString();
+ session.sendText(msgStr);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Notify comment error", e);
+ }
+ }
+ }
+
+ /**
+ * Called when the socket connection with the browser is established.
+ *
+ * @param session session
+ */
+ @Override
+ public void onConnect(final WebSocketSession session) {
+ final String articleId = session.getParameter(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(articleId)) {
+ return;
+ }
+
+ SESSIONS.add(session);
+
+ synchronized (ARTICLE_VIEWS) {
+ if (!ARTICLE_VIEWS.containsKey(articleId)) {
+ ARTICLE_VIEWS.put(articleId, 1);
+ } else {
+ final int count = ARTICLE_VIEWS.get(articleId);
+ ARTICLE_VIEWS.put(articleId, count + 1);
+ }
+ }
+
+ final JSONObject message = new JSONObject();
+ message.put(Article.ARTICLE_T_ID, articleId);
+ message.put(Common.OPERATION, "+");
+
+ ArticleListChannel.notifyHeat(message);
+ notifyHeat(message);
+ }
+
+ /**
+ * Called when the connection closed.
+ *
+ * @param session session
+ */
+ @Override
+ public void onClose(final WebSocketSession session) {
+ removeSession(session);
+ }
+
+ /**
+ * Called when a message received from the browser.
+ *
+ * @param message message
+ */
+ @Override
+ public void onMessage(final Message message) {
+ }
+
+ /**
+ * Called when a error received.
+ *
+ * @param error error
+ */
+ @Override
+ public void onError(final Error error) {
+ removeSession(error.session);
+ }
+
+ /**
+ * Removes the specified session.
+ *
+ * @param session the specified session
+ */
+ private void removeSession(final WebSocketSession session) {
+ SESSIONS.remove(session);
+
+ final String articleId = session.getParameter(Article.ARTICLE_T_ID);
+ if (StringUtils.isBlank(articleId)) {
+ return;
+ }
+
+ synchronized (ARTICLE_VIEWS) {
+ if (!ARTICLE_VIEWS.containsKey(articleId)) {
+ return;
+ }
+
+ final int count = ARTICLE_VIEWS.get(articleId);
+ final int newCount = count - 1;
+ if (newCount < 1) {
+ ARTICLE_VIEWS.remove(articleId);
+ } else {
+ ARTICLE_VIEWS.put(articleId, newCount);
+ }
+ }
+
+ final JSONObject message = new JSONObject();
+ message.put(Article.ARTICLE_T_ID, articleId);
+ message.put(Common.OPERATION, "-");
+
+ ArticleListChannel.notifyHeat(message);
+ notifyHeat(message);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/channel/ArticleListChannel.java b/src/main/java/org/b3log/symphony/processor/channel/ArticleListChannel.java
new file mode 100644
index 000000000..891242462
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/channel/ArticleListChannel.java
@@ -0,0 +1,118 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.channel;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.http.WebSocketChannel;
+import org.b3log.latke.http.WebSocketSession;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Article;
+import org.json.JSONObject;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Article list channel.
+ *
+ * @author Liang Ding
+ * @version 2.0.0.0, Nov 6, 2019
+ * @since 1.3.0
+ */
+@Singleton
+public class ArticleListChannel implements WebSocketChannel {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ArticleListChannel.class);
+
+ /**
+ * Session articles <session, "articleId1,articleId2">.
+ */
+ public static final Map SESSIONS = new ConcurrentHashMap<>();
+
+ /**
+ * Notifies the specified article heat message to browsers.
+ *
+ * @param message the specified message, for example
+ * {
+ * "articleId": "",
+ * "operation": "" // "+"/"-"
+ * }
+ */
+ public static void notifyHeat(final JSONObject message) {
+ final String articleId = message.optString(Article.ARTICLE_T_ID);
+ final String msgStr = message.toString();
+
+ for (final Map.Entry entry : SESSIONS.entrySet()) {
+ final WebSocketSession session = entry.getKey();
+ final String articleIds = entry.getValue();
+ if (!StringUtils.contains(articleIds, articleId)) {
+ continue;
+ }
+
+ session.sendText(msgStr);
+ }
+ }
+
+ /**
+ * Called when the socket connection with the browser is established.
+ *
+ * @param session session
+ */
+ @Override
+ public void onConnect(final WebSocketSession session) {
+ final String articleIds = session.getParameter(Article.ARTICLE_T_IDS);
+ if (StringUtils.isBlank(articleIds)) {
+ return;
+ }
+
+ SESSIONS.put(session, articleIds);
+ }
+
+ /**
+ * Called when the connection closed.
+ *
+ * @param session session
+ */
+ @Override
+ public void onClose(final WebSocketSession session) {
+ SESSIONS.remove(session);
+ }
+
+ /**
+ * Called when a message received from the browser.
+ *
+ * @param message message
+ */
+ @Override
+ public void onMessage(final Message message) {
+ }
+
+ /**
+ * Called when a error received.
+ *
+ * @param error error
+ */
+ @Override
+ public void onError(final Error error) {
+ SESSIONS.remove(error.session);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/channel/ChatroomChannel.java b/src/main/java/org/b3log/symphony/processor/channel/ChatroomChannel.java
new file mode 100644
index 000000000..a822b33da
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/channel/ChatroomChannel.java
@@ -0,0 +1,139 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.channel;
+
+import org.b3log.latke.http.WebSocketChannel;
+import org.b3log.latke.http.WebSocketSession;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.symphony.model.Common;
+import org.json.JSONObject;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Chatroom channel.
+ *
+ * @author Liang Ding
+ * @version 2.0.0.0, Nov 6, 2019
+ * @since 1.4.0
+ */
+@Singleton
+public class ChatroomChannel implements WebSocketChannel {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ChatroomChannel.class);
+
+ /**
+ * Session set.
+ */
+ public static final Set SESSIONS = Collections.newSetFromMap(new ConcurrentHashMap());
+
+ /**
+ * Called when the socket connection with the browser is established.
+ *
+ * @param session session
+ */
+ @Override
+ public void onConnect(final WebSocketSession session) {
+ SESSIONS.add(session);
+
+ synchronized (SESSIONS) {
+ final Iterator i = SESSIONS.iterator();
+ while (i.hasNext()) {
+ final WebSocketSession s = i.next();
+ final String msgStr = new JSONObject().put(Common.ONLINE_CHAT_CNT, SESSIONS.size()).put(Common.TYPE, "online").toString();
+ s.sendText(msgStr);
+ }
+ }
+ }
+
+ /**
+ * Called when the connection closed.
+ *
+ * @param session session
+ */
+ @Override
+ public void onClose(final WebSocketSession session) {
+ removeSession(session);
+ }
+
+ /**
+ * Called when a message received from the browser.
+ *
+ * @param message message
+ */
+ @Override
+ public void onMessage(final Message message) {
+ }
+
+ /**
+ * Called when a error received.
+ *
+ * @param error error
+ */
+ @Override
+ public void onError(final Error error) {
+ removeSession(error.session);
+ }
+
+ /**
+ * Notifies the specified chat message to browsers.
+ *
+ * @param message the specified message, for example,
+ * {
+ * "userName": "",
+ * "content": ""
+ * }
+ */
+ public static void notifyChat(final JSONObject message) {
+ message.put(Common.TYPE, "msg");
+ final String msgStr = message.toString();
+
+ synchronized (SESSIONS) {
+ final Iterator i = SESSIONS.iterator();
+ while (i.hasNext()) {
+ final WebSocketSession session = i.next();
+ session.sendText(msgStr);
+ }
+ }
+ }
+
+ /**
+ * Removes the specified session.
+ *
+ * @param session the specified session
+ */
+ private void removeSession(final WebSocketSession session) {
+ SESSIONS.remove(session);
+
+ synchronized (SESSIONS) {
+ final Iterator i = SESSIONS.iterator();
+ while (i.hasNext()) {
+ final WebSocketSession s = i.next();
+ final String msgStr = new JSONObject().put(Common.ONLINE_CHAT_CNT, SESSIONS.size()).put(Common.TYPE, "online").toString();
+ s.sendText(msgStr);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/processor/channel/GobangChannel.java b/src/main/java/org/b3log/symphony/processor/channel/GobangChannel.java
new file mode 100644
index 000000000..5aca359a8
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/channel/GobangChannel.java
@@ -0,0 +1,594 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.channel;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.WebSocketChannel;
+import org.b3log.latke.http.WebSocketSession;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.model.Pointtransfer;
+import org.b3log.symphony.service.ActivityMgmtService;
+import org.b3log.symphony.service.UserQueryService;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Gobang game channel.
+ *
+ * 状态值约定(为取值方便不做enum或者常量值了,当然日后或许重构)
+ * 1:聊天,2:下子,3:创建游戏,等待加入,4:加入游戏,游戏开始,5:断线重连,恢复棋盘,6:系统通知,7:请求和棋
+ *
+ *
+ * @author Zephyr
+ * @author Liang Ding
+ * @version 2.0.0.0, Nov 6, 2019
+ * @since 2.1.0
+ */
+@Singleton
+public class GobangChannel implements WebSocketChannel {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(GobangChannel.class);
+
+ /**
+ * Session set.
+ */
+ public static final Map SESSIONS = new ConcurrentHashMap<>();
+
+ /**
+ * 正在进行中的棋局.
+ * String参数代表开局者(选手1)的userId
+ * ChessGame参数代表棋局
+ */
+ public static final Map chessPlaying = new ConcurrentHashMap<>();
+
+ /**
+ * 对手,与正在进行的棋局Map配套使用.
+ * 第一个String代表player1,
+ * 第二个String代表player2
+ */
+ public static final Map antiPlayer = new ConcurrentHashMap<>();
+
+ /**
+ * 等待的棋局队列.
+ */
+ public static final Queue chessRandomWait = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Activity management service.
+ */
+ @Inject
+ private ActivityMgmtService activityMgmtService;
+
+ // 等待指定用户的棋局(暂不实现)
+
+ /**
+ * Called when the socket connection with the browser is established.
+ *
+ * @param session session
+ */
+ @Override
+ public void onConnect(final WebSocketSession session) {
+ final String userStr = session.getHttpSession().getAttribute(User.USER);
+ if (null == userStr) {
+ return;
+ }
+
+ final JSONObject user = new JSONObject(userStr);
+ final String userId = user.optString(Keys.OBJECT_ID);
+ final String userName = user.optString(User.USER_NAME);
+ boolean playing = false;
+ LOGGER.debug("new connection from " + userName);
+ if (SESSIONS.containsKey(userId)) {
+ JSONObject sendText = new JSONObject();
+ sendText.put("type", 6);
+ sendText.put("message", "【系统】:您已在匹配队列中,请勿开始多个游戏,如需打开新的窗口,请先关闭原窗口再开始");
+ session.sendText(sendText.toString());
+ return;
+ } else {
+ SESSIONS.put(userId, session);
+ }
+ for (String temp : chessPlaying.keySet()) {
+ ChessGame chessGame = chessPlaying.get(temp);
+ if (userId.equals(chessGame.getPlayer1())) { //玩家1返回战局
+ recoverGame(userId, userName, chessGame.getPlayer2(), chessGame);
+ chessGame.setPlayState1(true);
+ playing = true;
+ } else if (userId.equals(chessGame.getPlayer2())) { //玩家2返回战局
+ recoverGame(userId, userName, chessGame.getPlayer1(), chessGame);
+ chessGame.setPlayState2(true);
+ playing = true;
+ }
+ }
+ if (playing) {
+ return;
+ } else {
+ ChessGame chessGame = null;
+ JSONObject sendText = new JSONObject();
+
+ do {
+ chessGame = chessRandomWait.poll();
+ } while (chessRandomWait.size() > 0 && SESSIONS.get(chessGame.getPlayer1()) == null);
+
+ if (chessGame == null) {
+ chessGame = new ChessGame(userId, userName);
+ chessRandomWait.add(chessGame);
+ sendText.put("type", 3);
+ sendText.put("playerName", userName);
+ sendText.put("message", "【系统】:请等待另一名玩家加入游戏");
+ session.sendText(sendText.toString());
+ } else if (userId.equals(chessGame.getPlayer1())) { //仍然在匹配队列中
+ chessRandomWait.add(chessGame);//重新入队
+ sendText.put("type", 3);
+ sendText.put("playerName", userName);
+ sendText.put("message", "【系统】:请等待另一名玩家加入游戏");
+ session.sendText(sendText.toString());
+ } else {
+ final BeanManager beanManager = BeanManager.getInstance();
+ chessGame.setPlayer2(userId);
+ chessGame.setName2(userName);
+ chessGame.setPlayState2(true);
+ chessGame.setStep(1);
+ chessPlaying.put(chessGame.getPlayer1(), chessGame);
+ antiPlayer.put(chessGame.getPlayer1(), chessGame.getPlayer2());
+
+ final ActivityMgmtService activityMgmtService = beanManager.getReference(ActivityMgmtService.class);
+
+
+ sendText.put("type", 4);
+
+ //针对开局玩家的消息
+ sendText.put("message", "【系统】:玩家 [" + userName + "] 已加入,游戏开始,请落子");
+ sendText.put("player", chessGame.getPlayer1());
+
+ SESSIONS.get(chessGame.getPlayer1()).sendText(sendText.toString());
+ //针对参与玩家的消息
+ sendText.put("message", "游戏开始~!您正在与 [" + chessGame.getName1() + "] 对战");
+ sendText.put("player", chessGame.getPlayer2());
+ session.sendText(sendText.toString());
+
+ JSONObject r1 = activityMgmtService.startGobang(chessGame.getPlayer1());
+ JSONObject r2 = activityMgmtService.startGobang(chessGame.getPlayer2());
+ }
+ }
+ }
+
+ /**
+ * Called when the connection closed.
+ *
+ * @param session session
+ */
+ @Override
+ public void onClose(final WebSocketSession session) {
+ removeSession(session);
+ }
+
+ /**
+ * Called when a message received from the browser.
+ *
+ * @param message message
+ */
+ @Override
+ public void onMessage(final Message message) throws JSONException {
+ JSONObject jsonObject = new JSONObject(message);
+ final String player = jsonObject.optString("player");
+ final String anti = getAntiPlayer(player);
+ JSONObject sendText = new JSONObject();
+ final BeanManager beanManager = BeanManager.getInstance();
+ switch (jsonObject.optInt("type")) {
+ case 1: //聊天
+ LOGGER.debug(jsonObject.optString("message"));
+ final UserQueryService userQueryService = beanManager.getReference(UserQueryService.class);
+ sendText.put("type", 1);
+ sendText.put("player", userQueryService.getUser(player).optString(User.USER_NAME));
+ sendText.put("message", jsonObject.optString("message"));
+ SESSIONS.get(anti).sendText(sendText.toString());
+ break;
+ case 2: //落子
+ ChessGame chessGame = chessPlaying.keySet().contains(player) ? chessPlaying.get(player) : chessPlaying.get(anti);
+ int x = jsonObject.optInt("x");
+ int y = jsonObject.optInt("y");
+ int size = jsonObject.optInt("size");
+ if (chessGame != null) {
+ if (chessGame.getChess()[x / size][y / size] != 0) {
+ return;
+ }
+ boolean flag = false;
+ if (player.equals(chessGame.getPlayer1())) {
+ if (chessGame.getStep() != 1) {
+ return;
+ } else {
+ sendText.put("color", "black");
+ chessGame.getChess()[x / size][y / size] = 1;
+ flag = chessGame.chessCheck(1);
+ chessGame.setStep(2);
+ }
+ } else {
+ if (chessGame.getStep() != 2) {
+ return;
+ } else {
+ sendText.put("color", "white");
+ chessGame.getChess()[x / size][y / size] = 2;
+ flag = chessGame.chessCheck(2);
+ chessGame.setStep(1);
+ }
+ }
+ sendText.put("type", 2);
+ sendText.put("player", player);
+ sendText.put("posX", x);
+ sendText.put("posY", y);
+ sendText.put("chess", chessGame.getChess());
+ sendText.put("step", chessGame.getStep());
+ //chessPlaying是一个以玩家1为key的正在游戏的Map
+ //按道理,两个玩家不会出现在多个棋局(卧槽?好像一个人想跟多个人下也不是不讲道理啊……whatever)
+ //故当游戏结束时,可以按照player和anti移除两次(因为不知道哪个才是玩家1)
+ //总有一次能正确移除,分开写只是为了好看,没有逻辑原因
+ if (flag) {
+ sendText.put("result", "You Win");
+ chessPlaying.remove(player);
+ }
+ SESSIONS.get(player).sendText(sendText.toString());
+ if (flag) {
+ sendText.put("result", "You Lose");
+ chessPlaying.remove(anti);
+ }
+ SESSIONS.get(anti).sendText(sendText.toString());
+ if (flag) {
+ final ActivityMgmtService activityMgmtService = beanManager.getReference(ActivityMgmtService.class);
+ activityMgmtService.collectGobang(player, Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START * 2);
+ SESSIONS.remove(player);
+ SESSIONS.remove(anti);
+ }
+ }
+ break;
+ case 7://和棋
+ if ("request".equals(jsonObject.optString("drawType"))) {
+ sendText.put("type", 7);
+ SESSIONS.get(anti).sendText(sendText.toString());
+ } else if ("yes".equals(jsonObject.optString("drawType"))) {
+ sendText.put("type", 6);
+ sendText.put("message", "【系统】:双方和棋,积分返还,游戏结束");
+ chessPlaying.remove(player);
+ chessPlaying.remove(anti);
+ antiPlayer.remove(player);
+ antiPlayer.remove(anti);
+ final ActivityMgmtService activityMgmtService = beanManager.getReference(ActivityMgmtService.class);
+ activityMgmtService.collectGobang(player, Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START);
+ activityMgmtService.collectGobang(anti, Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START);
+ SESSIONS.get(player).sendText(sendText.toString());
+ SESSIONS.get(anti).sendText(sendText.toString());
+ SESSIONS.remove(player);
+ SESSIONS.remove(anti);
+ } else if ("no".equals(jsonObject.optString("drawType"))) {
+ sendText.put("type", 6);
+ sendText.put("message", "【系统】:对手拒绝和棋,请继续下棋");
+ SESSIONS.get(player).sendText(sendText.toString());
+ }
+ break;
+ }
+ }
+
+ /**
+ * Called when a error received.
+ *
+ * @param error error
+ */
+ @Override
+ public void onError(final Error error) {
+ removeSession(error.session);
+ }
+
+ /**
+ * Removes the specified session.
+ *
+ * @param session the specified session
+ */
+ private void removeSession(final WebSocketSession session) {
+ for (String player : SESSIONS.keySet()) {
+ if (session.equals(SESSIONS.get(player))) {
+ if (getAntiPlayer(player) == null) {
+ for (ChessGame chessGame : chessRandomWait) {
+ if (player.equals(chessGame.getPlayer1())) {
+ chessRandomWait.remove(chessGame);
+ }
+ }
+ } else {
+ if (chessPlaying.get(player) != null) { //说明玩家1断开了链接
+ ChessGame chessGame = chessPlaying.get(player);
+ chessGame.setPlayState1(false);
+ if (!chessGame.isPlayState2()) {
+ chessPlaying.remove(player);
+ antiPlayer.remove(player);
+ //由于玩家2先退出,补偿玩家1的积分
+ final BeanManager beanManager = BeanManager.getInstance();
+ final ActivityMgmtService activityMgmtService = beanManager.getReference(ActivityMgmtService.class);
+ activityMgmtService.collectGobang(chessGame.getPlayer1(), Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START);
+ } else {
+ JSONObject sendText = new JSONObject();
+ sendText.put("type", 6);
+ sendText.put("message", "【系统】:对手离开了棋局");
+ SESSIONS.get(chessGame.getPlayer2()).sendText(sendText.toString());
+ }
+ } else if (chessPlaying.get(getAntiPlayer(player)) != null) { //说明玩家2断开了链接
+ String player1 = getAntiPlayer(player);
+ ChessGame chessGame = chessPlaying.get(player1);
+ chessGame.setPlayState2(false);
+ if (!chessGame.isPlayState1()) {
+ chessPlaying.remove(player1);
+ antiPlayer.remove(player1);
+ //由于玩家1先退出,补偿玩家2的积分
+ final BeanManager beanManager = BeanManager.getInstance();
+ final ActivityMgmtService activityMgmtService = beanManager.getReference(ActivityMgmtService.class);
+ activityMgmtService.collectGobang(chessGame.getPlayer2(), Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START);
+ } else {
+ JSONObject sendText = new JSONObject();
+ sendText.put("type", 6);
+ sendText.put("message", "【系统】:对手离开了棋局");
+ SESSIONS.get(chessGame.getPlayer1()).sendText(sendText.toString());
+ }
+ }
+ }
+ SESSIONS.remove(player);
+ }
+ }
+ }
+
+ private String getAntiPlayer(String player) {
+ String anti = antiPlayer.get(player);
+ if (null == anti || anti.equals("")) {
+ for (String temp : antiPlayer.keySet()) {
+ if (player.equals(antiPlayer.get(temp))) {
+ anti = temp;
+ }
+ }
+ }
+ return anti;
+ }
+
+ private void recoverGame(String userId, String userName, String antiUserId, ChessGame chessGame) {
+ JSONObject sendText = new JSONObject();
+ sendText.put("type", 5);
+ sendText.put("chess", chessGame.getChess());
+ sendText.put("message", "【系统】:恢复棋盘,当前轮到玩家 [" + (chessGame.getStep() == 1 ? chessGame.getName1() : chessGame.getName2()) + "] 落子");
+ sendText.put("playerName", userName);
+ sendText.put("player", userId);
+ SESSIONS.get(userId).sendText(sendText.toString());
+ sendText = new JSONObject();
+ sendText.put("type", 6);
+ sendText.put("message", "【系统】:对手返回了棋局,当前轮到玩家 [" + (chessGame.getStep() == 1 ? chessGame.getName1() : chessGame.getName2()) + "] 落子");
+ SESSIONS.get(antiUserId).sendText(sendText.toString());
+ }
+}
+
+class ChessGame {
+ private long chessId;
+ private String player1;
+ private String player2;
+ private String name1;
+ private String name2;
+ private boolean playState1;
+ private boolean playState2;
+ private int state;//0空桌,1,等待,2满员
+ private int[][] chess = null;
+ private int step;//1-player1,2-player2;
+ private long starttime;
+
+ public ChessGame(String player1, String name1) {
+ this.chessId = System.currentTimeMillis();
+ this.player1 = player1;
+ this.name1 = name1;
+ this.playState1 = true;
+ this.playState2 = false;
+ this.chess = new int[20][20];
+ this.starttime = System.currentTimeMillis();
+ for (int i = 0; i < 20; i++) {
+ for (int j = 0; j < 20; j++) {
+ chess[i][j] = 0;
+ }
+ }
+ }
+
+ public boolean chessCheck(int step) {
+ //横向检查
+ for (int i = 0; i < this.chess.length; i++) {
+ int count = 0;
+ for (int j = 0; j < this.chess[i].length; j++) {
+ if (this.chess[i][j] == step) {
+ count++;
+ } else if (this.chess[i][j] != step && count < 5) {
+ count = 0;
+ }
+ }
+ if (count >= 5) {
+ return true;
+ }
+ }
+ //纵向检查
+ for (int j = 0; j < this.chess[0].length; j++) {
+ int count = 0;
+ for (int i = 0; i < this.chess.length; i++) {
+ if (this.chess[i][j] == step) {
+ count++;
+ } else if (this.chess[i][j] != step && count < 5) {
+ count = 0;
+ }
+ }
+ if (count >= 5) {
+ return true;
+ }
+ }
+ //左上右下检查,下一个检查点时上一个检查点横纵坐标均+1
+ //横向增长,横坐标先行出局
+ for (int x = 0, y = 0; x < this.chess.length; x++) {
+ int count = 0;
+ for (int i = x, j = y; i < this.chess.length; i++, j++) {
+ if (this.chess[i][j] == step) {
+ count++;
+ } else if (this.chess[i][j] != step && count < 5) {
+ count = 0;
+ }
+ }
+ if (count >= 5) {
+ return true;
+ }
+ }
+ //纵向增长,纵坐标先出局
+ for (int x = 0, y = 0; y < this.chess[0].length; y++) {
+ int count = 0;
+ for (int i = x, j = y; j < this.chess.length; i++, j++) {
+ if (this.chess[i][j] == step) {
+ count++;
+ } else if (this.chess[i][j] != step && count < 5) {
+ count = 0;
+ }
+ }
+ if (count >= 5) {
+ return true;
+ }
+ }
+ //左下右上检查x-1,y+1
+ //横向增长,横坐标先行出局
+ for (int x = 0, y = 0; x < this.chess.length; x++) {
+ int count = 0;
+ for (int i = x, j = y; i >= 0; i--, j++) {
+ if (this.chess[i][j] == step) {
+ count++;
+ } else if (this.chess[i][j] != step && count < 5) {
+ count = 0;
+ }
+ }
+ if (count >= 5) {
+ return true;
+ }
+ }
+ //纵向增长,纵坐标先出局
+ for (int x = this.chess.length - 1, y = 0; y < this.chess[0].length; y++) {
+ int count = 0;
+ for (int i = x, j = y; j < this.chess.length; i--, j++) {
+ if (this.chess[i][j] == step) {
+ count++;
+ } else if (this.chess[i][j] != step && count < 5) {
+ count = 0;
+ }
+ }
+ if (count >= 5) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public long getChessId() {
+ return chessId;
+ }
+
+ public void setChessId(long chessId) {
+ this.chessId = chessId;
+ }
+
+ public String getPlayer1() {
+ return player1;
+ }
+
+ public void setPlayer1(String player1) {
+ this.player1 = player1;
+ }
+
+ public String getPlayer2() {
+ return player2;
+ }
+
+ public void setPlayer2(String player2) {
+ this.player2 = player2;
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ public void setState(int state) {
+ this.state = state;
+ }
+
+ public int getStep() {
+ return step;
+ }
+
+ public void setStep(int step) {
+ this.step = step;
+ }
+
+ public int[][] getChess() {
+ return chess;
+ }
+
+ public void setChess(int[][] chess) {
+ this.chess = chess;
+ }
+
+ public long getStarttime() {
+ return starttime;
+ }
+
+ public void setStarttime(long starttime) {
+ this.starttime = starttime;
+ }
+
+ public boolean isPlayState1() {
+ return playState1;
+ }
+
+ public void setPlayState1(boolean playState1) {
+ this.playState1 = playState1;
+ }
+
+ public boolean isPlayState2() {
+ return playState2;
+ }
+
+ public void setPlayState2(boolean playState2) {
+ this.playState2 = playState2;
+ }
+
+ public String getName1() {
+ return name1;
+ }
+
+ public void setName1(String name1) {
+ this.name1 = name1;
+ }
+
+ public String getName2() {
+ return name2;
+ }
+
+ public void setName2(String name2) {
+ this.name2 = name2;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/b3log/symphony/processor/channel/UserChannel.java b/src/main/java/org/b3log/symphony/processor/channel/UserChannel.java
new file mode 100644
index 000000000..56fde8dca
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/processor/channel/UserChannel.java
@@ -0,0 +1,182 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.processor.channel;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.http.Session;
+import org.b3log.latke.http.WebSocketChannel;
+import org.b3log.latke.http.WebSocketSession;
+import org.b3log.latke.ioc.BeanManager;
+import org.b3log.latke.ioc.Singleton;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.model.User;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.service.UserMgmtService;
+import org.json.JSONObject;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * User channel.
+ *
+ * @author Liang Ding
+ * @version 2.0.0.0, Nov 6, 2019
+ * @since 1.4.0
+ */
+@Singleton
+public class UserChannel implements WebSocketChannel {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(UserChannel.class);
+
+ /**
+ * Session set.
+ */
+ public static final Map> SESSIONS = new ConcurrentHashMap();
+
+ /**
+ * Called when the socket connection with the browser is established.
+ *
+ * @param session session
+ */
+ @Override
+ public void onConnect(final WebSocketSession session) {
+ final Session httpSession = session.getHttpSession();
+
+ final JSONObject user = new JSONObject(httpSession.getAttribute(User.USER));
+ if (null == user) {
+ return;
+ }
+
+ final String userId = user.optString(Keys.OBJECT_ID);
+ final Set userSessions = SESSIONS.getOrDefault(userId, Collections.newSetFromMap(new ConcurrentHashMap()));
+ userSessions.add(session);
+
+ SESSIONS.put(userId, userSessions);
+
+ final BeanManager beanManager = BeanManager.getInstance();
+ final UserMgmtService userMgmtService = beanManager.getReference(UserMgmtService.class);
+ final String ip = httpSession.getAttribute(Common.IP);
+ userMgmtService.updateOnlineStatus(userId, ip, true, true);
+ }
+
+ /**
+ * Called when the connection closed.
+ *
+ * @param session session
+ */
+ @Override
+ public void onClose(final WebSocketSession session) {
+ removeSession(session);
+ }
+
+ /**
+ * Called when a message received from the browser.
+ *
+ * @param message message
+ */
+ @Override
+ public void onMessage(final Message message) {
+ final Session session = message.session.getHttpSession();
+ final String userStr = session.getAttribute(User.USER);
+ if (null == userStr) {
+ return;
+ }
+ final JSONObject user = new JSONObject(userStr);
+
+ final String userId = user.optString(Keys.OBJECT_ID);
+ final BeanManager beanManager = BeanManager.getInstance();
+ final UserMgmtService userMgmtService = beanManager.getReference(UserMgmtService.class);
+ final String ip = session.getAttribute(Common.IP);
+ userMgmtService.updateOnlineStatus(userId, ip, true, true);
+ }
+
+ /**
+ * Called when a error received.
+ *
+ * @param error error
+ */
+ @Override
+ public void onError(final Error error) {
+ removeSession(error.session);
+ }
+
+ /**
+ * Sends command to browsers.
+ *
+ * @param message the specified message, for example,
+ * "userId": "",
+ * "cmd": ""
+ */
+ public static void sendCmd(final JSONObject message) {
+ final String recvUserId = message.optString(UserExt.USER_T_ID);
+ if (StringUtils.isBlank(recvUserId)) {
+ return;
+ }
+
+ final String msgStr = message.toString();
+
+ for (final String userId : SESSIONS.keySet()) {
+ if (userId.equals(recvUserId)) {
+ final Set sessions = SESSIONS.get(userId);
+ for (final WebSocketSession session : sessions) {
+ session.sendText(msgStr);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes the specified session.
+ *
+ * @param session the specified session
+ */
+ private void removeSession(final WebSocketSession session) {
+ final Session httpSession = session.getHttpSession();
+ final String userStr = httpSession.getAttribute(User.USER);
+ if (null == userStr) {
+ return;
+ }
+ final JSONObject user = new JSONObject(userStr);
+ final String userId = user.optString(Keys.OBJECT_ID);
+ final BeanManager beanManager = BeanManager.getInstance();
+ final UserMgmtService userMgmtService = beanManager.getReference(UserMgmtService.class);
+ final String ip = httpSession.getAttribute(Common.IP);
+
+ Set userSessions = SESSIONS.get(userId);
+ if (null == userSessions) {
+ userMgmtService.updateOnlineStatus(userId, ip, false, false);
+
+ return;
+ }
+
+ userSessions.remove(session);
+ if (userSessions.isEmpty()) {
+ userMgmtService.updateOnlineStatus(userId, ip, false, false);
+
+ return;
+ }
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/ArticleRepository.java b/src/main/java/org/b3log/symphony/repository/ArticleRepository.java
new file mode 100644
index 000000000..6badf819c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/ArticleRepository.java
@@ -0,0 +1,141 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.cache.ArticleCache;
+import org.b3log.symphony.model.Article;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Article repository.
+ *
+ * @author Liang Ding
+ * @author qiankunpingtai
+ * @version 1.1.1.9, Jun 6, 2019
+ * @since 0.2.0
+ */
+@Repository
+public class ArticleRepository extends AbstractRepository {
+
+ /**
+ * Article cache.
+ */
+ @Inject
+ private ArticleCache articleCache;
+
+ /**
+ * Public constructor.
+ */
+ public ArticleRepository() {
+ super(Article.ARTICLE);
+ }
+
+ @Override
+ public void remove(final String id) throws RepositoryException {
+ super.remove(id);
+
+ articleCache.removeArticle(id);
+ }
+
+ @Override
+ public JSONObject get(final String id) throws RepositoryException {
+ JSONObject ret = articleCache.getArticle(id);
+ if (null != ret) {
+ return ret;
+ }
+
+ ret = super.get(id);
+ if (null == ret) {
+ return null;
+ }
+
+ articleCache.putArticle(ret);
+
+ return ret;
+ }
+
+ @Override
+ public void update(final String id, final JSONObject article, final String... propertyNames) throws RepositoryException {
+ super.update(id, article, propertyNames);
+
+ article.put(Keys.OBJECT_ID, id);
+ articleCache.putArticle(article);
+ }
+
+ @Override
+ public List getRandomly(final int fetchSize) throws RepositoryException {
+ final List ret = new ArrayList<>();
+
+ final double mid = Math.random();
+ Query query = new Query().
+ setFilter(CompositeFilterOperator.and(new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.GREATER_THAN_OR_EQUAL, mid),
+ new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.LESS_THAN_OR_EQUAL, mid),
+ new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.NOT_EQUAL, Article.ARTICLE_STATUS_C_INVALID),
+ new PropertyFilter(Article.ARTICLE_TYPE, FilterOperator.NOT_EQUAL, Article.ARTICLE_TYPE_C_DISCUSSION),
+ new PropertyFilter(Article.ARTICLE_SHOW_IN_LIST, FilterOperator.NOT_EQUAL, Article.ARTICLE_SHOW_IN_LIST_C_NOT))).
+ select(Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK, Article.ARTICLE_AUTHOR_ID).
+ setPage(1, fetchSize).setPageCount(1);
+ final List list1 = getList(query);
+ ret.addAll(list1);
+
+ final int reminingSize = fetchSize - list1.size();
+ if (0 != reminingSize) { // Query for remains
+ query = new Query().
+ setFilter(CompositeFilterOperator.and(new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.GREATER_THAN_OR_EQUAL, 0D),
+ new PropertyFilter(Article.ARTICLE_RANDOM_DOUBLE, FilterOperator.LESS_THAN_OR_EQUAL, mid),
+ new PropertyFilter(Article.ARTICLE_STATUS, FilterOperator.NOT_EQUAL, Article.ARTICLE_STATUS_C_INVALID),
+ new PropertyFilter(Article.ARTICLE_TYPE, FilterOperator.NOT_EQUAL, Article.ARTICLE_TYPE_C_DISCUSSION),
+ new PropertyFilter(Article.ARTICLE_SHOW_IN_LIST, FilterOperator.NOT_EQUAL, Article.ARTICLE_SHOW_IN_LIST_C_NOT))).
+ select(Article.ARTICLE_TITLE, Article.ARTICLE_PERMALINK, Article.ARTICLE_AUTHOR_ID).
+ setPage(1, reminingSize).setPageCount(1);
+ final List list2 = getList(query);
+
+ ret.addAll(list2);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Gets an article by the specified article title.
+ *
+ * @param articleTitle the specified article title
+ * @return an article, {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTitle(final String articleTitle) throws RepositoryException {
+ final Query query = new Query().setFilter(new PropertyFilter(Article.ARTICLE_TITLE,
+ FilterOperator.EQUAL, articleTitle)).setPageCount(1);
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/BreezemoonRepository.java b/src/main/java/org/b3log/symphony/repository/BreezemoonRepository.java
new file mode 100644
index 000000000..d71156d91
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/BreezemoonRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Breezemoon;
+
+/**
+ * Breezemoon repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, May 21, 2018
+ * @since 2.8.0
+ */
+@Repository
+public class BreezemoonRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public BreezemoonRepository() {
+ super(Breezemoon.BREEZEMOON);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/CharacterRepository.java b/src/main/java/org/b3log/symphony/repository/CharacterRepository.java
new file mode 100644
index 000000000..d2793b92c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/CharacterRepository.java
@@ -0,0 +1,39 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+
+/**
+ * Character repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jun 8, 2016
+ * @since 1.4.0
+ */
+@Repository
+public class CharacterRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public CharacterRepository() {
+ super(org.b3log.symphony.model.Character.CHARACTER);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/CommentRepository.java b/src/main/java/org/b3log/symphony/repository/CommentRepository.java
new file mode 100644
index 000000000..c65b43bbf
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/CommentRepository.java
@@ -0,0 +1,184 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.apache.commons.lang.StringUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.model.User;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.cache.CommentCache;
+import org.b3log.symphony.model.*;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Comment repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.1.3, Jun 6, 2019
+ * @since 0.2.0
+ */
+@Repository
+public class CommentRepository extends AbstractRepository {
+
+ /**
+ * Comment cache.
+ */
+ @Inject
+ private CommentCache commentCache;
+
+ /**
+ * Article repository.
+ */
+ @Inject
+ private ArticleRepository articleRepository;
+
+ /**
+ * User repository.
+ */
+ @Inject
+ private UserRepository userRepository;
+
+ /**
+ * Revision repository.
+ */
+ @Inject
+ private RevisionRepository revisionRepository;
+
+ /**
+ * Option repository.
+ */
+ @Inject
+ private OptionRepository optionRepository;
+
+ /**
+ * Notification repository.
+ */
+ @Inject
+ private NotificationRepository notificationRepository;
+
+ /**
+ * Public constructor.
+ */
+ public CommentRepository() {
+ super(Comment.COMMENT);
+ }
+
+ /**
+ * Removes a comment specified with the given comment id. Calls this method will remove all existed data related
+ * with the specified comment forcibly.
+ *
+ * @param commentId the given comment id
+ * @throws RepositoryException repository exception
+ */
+ public void removeComment(final String commentId) throws RepositoryException {
+ final JSONObject comment = get(commentId);
+ if (null == comment) {
+ return;
+ }
+
+ remove(comment.optString(Keys.OBJECT_ID));
+
+ final String commentAuthorId = comment.optString(Comment.COMMENT_AUTHOR_ID);
+ final JSONObject commenter = userRepository.get(commentAuthorId);
+ int commentCount = commenter.optInt(UserExt.USER_COMMENT_COUNT) - 1;
+ if (0 > commentCount) {
+ commentCount = 0;
+ }
+ commenter.put(UserExt.USER_COMMENT_COUNT, commentCount);
+ userRepository.update(commentAuthorId, commenter);
+
+ final String articleId = comment.optString(Comment.COMMENT_ON_ARTICLE_ID);
+ final JSONObject article = articleRepository.get(articleId);
+ article.put(Article.ARTICLE_COMMENT_CNT, article.optInt(Article.ARTICLE_COMMENT_CNT) - 1);
+ if (0 < article.optInt(Article.ARTICLE_COMMENT_CNT)) {
+ final Query latestCmtQuery = new Query().
+ setFilter(new PropertyFilter(Comment.COMMENT_ON_ARTICLE_ID, FilterOperator.EQUAL, articleId)).
+ addSort(Keys.OBJECT_ID, SortDirection.DESCENDING).
+ setPage(1, 1);
+ final JSONObject latestCmt = get(latestCmtQuery).optJSONArray(Keys.RESULTS).optJSONObject(0);
+ article.put(Article.ARTICLE_LATEST_CMT_TIME, latestCmt.optLong(Keys.OBJECT_ID));
+ final JSONObject latestCmtAuthor = userRepository.get(latestCmt.optString(Comment.COMMENT_AUTHOR_ID));
+ article.put(Article.ARTICLE_LATEST_CMTER_NAME, latestCmtAuthor.optString(User.USER_NAME));
+ } else {
+ article.put(Article.ARTICLE_LATEST_CMT_TIME, articleId);
+ article.put(Article.ARTICLE_LATEST_CMTER_NAME, "");
+ }
+ articleRepository.update(articleId, article);
+
+ final Query query = new Query().setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Revision.REVISION_DATA_ID, FilterOperator.EQUAL, commentId),
+ new PropertyFilter(Revision.REVISION_DATA_TYPE, FilterOperator.EQUAL, Revision.DATA_TYPE_C_COMMENT)
+ ));
+ final JSONArray commentRevisions = revisionRepository.get(query).optJSONArray(Keys.RESULTS);
+ for (int j = 0; j < commentRevisions.length(); j++) {
+ final JSONObject articleRevision = commentRevisions.optJSONObject(j);
+ revisionRepository.remove(articleRevision.optString(Keys.OBJECT_ID));
+ }
+
+ final JSONObject commentCntOption = optionRepository.get(Option.ID_C_STATISTIC_CMT_COUNT);
+ commentCntOption.put(Option.OPTION_VALUE, commentCntOption.optInt(Option.OPTION_VALUE) - 1);
+ optionRepository.update(Option.ID_C_STATISTIC_CMT_COUNT, commentCntOption);
+
+ final String originalCommentId = comment.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID);
+ if (StringUtils.isNotBlank(originalCommentId)) {
+ final JSONObject originalComment = get(originalCommentId);
+ if (null != originalComment) {
+ originalComment.put(Comment.COMMENT_REPLY_CNT, originalComment.optInt(Comment.COMMENT_REPLY_CNT) - 1);
+
+ update(originalCommentId, originalComment);
+ }
+ }
+
+ notificationRepository.removeByDataId(commentId);
+ }
+
+ @Override
+ public void remove(final String id) throws RepositoryException {
+ super.remove(id);
+
+ commentCache.removeComment(id);
+ }
+
+ @Override
+ public JSONObject get(final String id) throws RepositoryException {
+ JSONObject ret = commentCache.getComment(id);
+ if (null != ret) {
+ return ret;
+ }
+
+ ret = super.get(id);
+ if (null == ret) {
+ return null;
+ }
+
+ commentCache.putComment(ret);
+
+ return ret;
+ }
+
+ @Override
+ public void update(final String id, final JSONObject comment, final String... propertyNames) throws RepositoryException {
+ super.update(id, comment, propertyNames);
+
+ comment.put(Keys.OBJECT_ID, id);
+ commentCache.putComment(comment);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/DomainRepository.java b/src/main/java/org/b3log/symphony/repository/DomainRepository.java
new file mode 100644
index 000000000..cdb829684
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/DomainRepository.java
@@ -0,0 +1,83 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Domain;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Domain repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Mar 13, 2013
+ * @since 1.4.0
+ */
+@Repository
+public class DomainRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public DomainRepository() {
+ super(Domain.DOMAIN);
+ }
+
+ /**
+ * Gets a domain by the specified domain title.
+ *
+ * @param domainTitle the specified domain title
+ * @return a domain, {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTitle(final String domainTitle) throws RepositoryException {
+ final Query query = new Query().setFilter(new PropertyFilter(Domain.DOMAIN_TITLE, FilterOperator.EQUAL, domainTitle)).setPageCount(1);
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+
+ /**
+ * Gets a domain by the specified domain URI.
+ *
+ * @param domainURI the specified domain URI
+ * @return a domain, {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByURI(final String domainURI) throws RepositoryException {
+ final Query query = new Query().setFilter(new PropertyFilter(Domain.DOMAIN_URI, FilterOperator.EQUAL, domainURI)).setPageCount(1);
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/DomainTagRepository.java b/src/main/java/org/b3log/symphony/repository/DomainTagRepository.java
new file mode 100644
index 000000000..6bd3e32bb
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/DomainTagRepository.java
@@ -0,0 +1,118 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Domain;
+import org.b3log.symphony.model.Tag;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Domain-Tag relation repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Apr 12, 2016
+ * @since 1.4.0
+ */
+@Repository
+public class DomainTagRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public DomainTagRepository() {
+ super(Domain.DOMAIN + "_" + Tag.TAG);
+ }
+
+ /**
+ * Gets domain-tag relations by the specified domain id.
+ *
+ * @param domainId the specified domain id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "domain_oId": domainId,
+ * "tag_oId": ""
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByDomainId(final String domainId, final int currentPageNum, final int pageSize)
+ throws RepositoryException {
+ final Query query = new Query().
+ setFilter(new PropertyFilter(Domain.DOMAIN + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, domainId)).
+ setPage(currentPageNum, pageSize).setPageCount(1);
+
+ return get(query);
+ }
+
+ /**
+ * Removes domain-tag relations by the specified domain id.
+ *
+ * @param domainId the specified domain id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByDomainId(final String domainId) throws RepositoryException {
+ final Query query = new Query().
+ setFilter(new PropertyFilter(Domain.DOMAIN + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, domainId));
+ final JSONArray relations = get(query).optJSONArray(Keys.RESULTS);
+ for (int i = 0; i < relations.length(); i++) {
+ final JSONObject rel = relations.optJSONObject(i);
+ remove(rel.optString(Keys.OBJECT_ID));
+ }
+ }
+
+ /**
+ * Gets domain-tag relations by the specified tag id.
+ *
+ * @param tagId the specified tag id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "domain_oId": "",
+ * "tag_oId": tagId
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTagId(final String tagId, final int currentPageNum, final int pageSize)
+ throws RepositoryException {
+ final Query query = new Query().
+ setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)).
+ setPage(currentPageNum, pageSize).setPageCount(1);
+
+ return get(query);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/EmotionRepository.java b/src/main/java/org/b3log/symphony/repository/EmotionRepository.java
new file mode 100644
index 000000000..6fbfed75b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/EmotionRepository.java
@@ -0,0 +1,84 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Emotion;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Emotion repository.
+ *
+ * @author Zephyr
+ * @author Liang Ding
+ * @version 1.0.1.1, Mar 5, 2019
+ * @since 1.5.0
+ */
+@Repository
+public class EmotionRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public EmotionRepository() {
+ super(Emotion.EMOTION);
+ }
+
+ /**
+ * Gets a user's emotion (emoji with type=0).
+ *
+ * @param userId the specified user id
+ * @return emoji string join with {@code ","}, returns {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public String getUserEmojis(final String userId) throws RepositoryException {
+ final Query query = new Query().setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Emotion.EMOTION_USER_ID, FilterOperator.EQUAL, userId),
+ new PropertyFilter(Emotion.EMOTION_TYPE, FilterOperator.EQUAL, Emotion.EMOTION_TYPE_C_EMOJI)
+ )).addSort(Emotion.EMOTION_SORT, SortDirection.ASCENDING);
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+ if (0 == array.length()) {
+ return null;
+ }
+
+ final StringBuilder retBuilder = new StringBuilder();
+ for (int i = 0; i < array.length(); i++) {
+ retBuilder.append(array.optJSONObject(i).optString(Emotion.EMOTION_CONTENT));
+ if (i != array.length() - 1) {
+ retBuilder.append(",");
+ }
+ }
+
+ return retBuilder.toString();
+ }
+
+ /**
+ * Remove emotions by the specified user id.
+ *
+ * @param userId the specified user id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByUserId(final String userId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Emotion.EMOTION_USER_ID, FilterOperator.EQUAL, userId)));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/FollowRepository.java b/src/main/java/org/b3log/symphony/repository/FollowRepository.java
new file mode 100644
index 000000000..7ae2e6d91
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/FollowRepository.java
@@ -0,0 +1,136 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Follow;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Follow repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Dec 18, 2018
+ * @since 0.2.5
+ */
+@Repository
+public class FollowRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public FollowRepository() {
+ super(Follow.FOLLOW);
+ }
+
+ /**
+ * Removes a follow relationship by the specified follower id and the specified following entity id.
+ *
+ * @param followerId the specified follower id
+ * @param followingId the specified following entity id
+ * @param followingType the specified following type
+ * @throws RepositoryException repository exception
+ */
+ public void removeByFollowerIdAndFollowingId(final String followerId, final String followingId, final int followingType)
+ throws RepositoryException {
+ final JSONObject toRemove = getByFollowerIdAndFollowingId(followerId, followingId, followingType);
+
+ if (null == toRemove) {
+ return;
+ }
+
+ remove(toRemove.optString(Keys.OBJECT_ID));
+ }
+
+ /**
+ * Gets a follow relationship by the specified follower id and the specified following entity id.
+ *
+ * @param followerId the specified follower id
+ * @param followingId the specified following entity id
+ * @param followingType the specified following type
+ * @return follow relationship, returns {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByFollowerIdAndFollowingId(final String followerId, final String followingId, final int followingType)
+ throws RepositoryException {
+ final List filters = new ArrayList();
+ filters.add(new PropertyFilter(Follow.FOLLOWER_ID, FilterOperator.EQUAL, followerId));
+ filters.add(new PropertyFilter(Follow.FOLLOWING_ID, FilterOperator.EQUAL, followingId));
+ filters.add(new PropertyFilter(Follow.FOLLOWING_TYPE, FilterOperator.EQUAL, followingType));
+
+ final Query query = new Query().setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters));
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+
+ /**
+ * Determines whether exists a follow relationship for the specified follower and the specified following entity.
+ *
+ * @param followerId the specified follower id
+ * @param followingId the specified following entity id
+ * @param followingType the specified following type
+ * @return {@code true} if exists, returns {@code false} otherwise
+ * @throws RepositoryException repository exception
+ */
+ public boolean exists(final String followerId, final String followingId, final int followingType)
+ throws RepositoryException {
+ return null != getByFollowerIdAndFollowingId(followerId, followingId, followingType);
+ }
+
+ /**
+ * Get follows by the specified following id and type.
+ *
+ * @param followingId the specified following id
+ * @param type the specified type
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * Follow
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByFollowingId(final String followingId, final int type, final int currentPageNum, final int pageSize) throws RepositoryException {
+ final Query query = new Query().
+ setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Follow.FOLLOWING_ID, FilterOperator.EQUAL, followingId),
+ new PropertyFilter(Follow.FOLLOWING_TYPE, FilterOperator.EQUAL, type))).
+ setPage(currentPageNum, pageSize).setPageCount(1);
+
+ return get(query);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/InvitecodeRepository.java b/src/main/java/org/b3log/symphony/repository/InvitecodeRepository.java
new file mode 100644
index 000000000..9657a7687
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/InvitecodeRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Invitecode;
+
+/**
+ * Invitecode repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jul 2, 2016
+ * @since 1.4.0
+ */
+@Repository
+public class InvitecodeRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public InvitecodeRepository() {
+ super(Invitecode.INVITECODE);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/LinkRepository.java b/src/main/java/org/b3log/symphony/repository/LinkRepository.java
new file mode 100644
index 000000000..dade2ada2
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/LinkRepository.java
@@ -0,0 +1,79 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.FilterOperator;
+import org.b3log.latke.repository.PropertyFilter;
+import org.b3log.latke.repository.Query;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Link;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Link repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Oct 1, 2018
+ * @since 1.6.0
+ */
+@Repository
+public class LinkRepository extends AbstractRepository {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(LinkRepository.class);
+
+ /**
+ * Gets a link with the specified address.
+ *
+ * @param addr the specified address
+ * @return a link, returns {@code null} if not found
+ */
+ public JSONObject getLink(final String addr) {
+ final String hash = DigestUtils.sha1Hex(addr);
+ final Query query = new Query().setFilter(new PropertyFilter(Link.LINK_ADDR_HASH, FilterOperator.EQUAL, hash)).
+ setPageCount(1).setPage(1, 1);
+ try {
+ final JSONObject result = get(query);
+ final JSONArray links = result.optJSONArray(Keys.RESULTS);
+ if (0 == links.length()) {
+ return null;
+ }
+
+ return links.optJSONObject(0);
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Gets link by address [addr=" + addr + ", hash=" + hash + "] failed", e);
+
+ return null;
+ }
+ }
+
+ /**
+ * Public constructor.
+ */
+ public LinkRepository() {
+ super(Link.LINK);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/LivenessRepository.java b/src/main/java/org/b3log/symphony/repository/LivenessRepository.java
new file mode 100644
index 000000000..72d5f0479
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/LivenessRepository.java
@@ -0,0 +1,76 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Liveness;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Liveness repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Feb 27, 2019
+ * @since 1.4.0
+ */
+@Repository
+public class LivenessRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public LivenessRepository() {
+ super(Liveness.LIVENESS);
+ }
+
+ /**
+ * Remove liveness by the specified user id.
+ *
+ * @param userId the specified user id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByUserId(final String userId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Liveness.LIVENESS_USER_ID, FilterOperator.EQUAL, userId)));
+ }
+
+ /**
+ * Gets a liveness by the specified user id and date.
+ *
+ * @param userId the specified user id
+ * @param date the specified date
+ * @return a liveness, {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByUserAndDate(final String userId, final String date) throws RepositoryException {
+ final Query query = new Query().setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Liveness.LIVENESS_USER_ID, FilterOperator.EQUAL, userId),
+ new PropertyFilter(Liveness.LIVENESS_DATE, FilterOperator.EQUAL, date))).setPageCount(1);
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/NotificationRepository.java b/src/main/java/org/b3log/symphony/repository/NotificationRepository.java
new file mode 100644
index 000000000..4f2fee93c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/NotificationRepository.java
@@ -0,0 +1,88 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Notification;
+
+/**
+ * Notification repository.
+ *
+ * @author Liang Ding
+ * @version 2.2.0.0, Feb 27, 2019
+ * @since 0.2.5
+ */
+@Repository
+public class NotificationRepository extends AbstractRepository {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(NotificationRepository.class);
+
+ /**
+ * Public constructor.
+ */
+ public NotificationRepository() {
+ super(Notification.NOTIFICATION);
+ }
+
+ /**
+ * Removes notifications by the specified data id.
+ *
+ * @param dataId the specified data id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByDataId(final String dataId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Notification.NOTIFICATION_DATA_ID, FilterOperator.EQUAL, dataId)));
+ }
+
+ /**
+ * Checks whether has sent a notification to a user specified by the given user id with the specified data id and data type.
+ *
+ * @param userId the given user id
+ * @param dataId the specified the specified data id
+ * @param notificationDataType the specified notification data type
+ * @return {@code true} if sent, returns {@code false} otherwise
+ */
+ public boolean hasSentByDataIdAndType(final String userId, final String dataId, final int notificationDataType) {
+ try {
+ return 0 < count(new Query().setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Notification.NOTIFICATION_USER_ID, FilterOperator.EQUAL, userId),
+ new PropertyFilter(Notification.NOTIFICATION_DATA_ID, FilterOperator.EQUAL, dataId),
+ new PropertyFilter(Notification.NOTIFICATION_DATA_TYPE, FilterOperator.EQUAL, notificationDataType))));
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Checks [" + notificationDataType + "] notification sent failed [userId=" + userId + ", dataId=" + dataId + "]", e);
+
+ return false;
+ }
+ }
+
+ /**
+ * Remove notifications by the specified user id.
+ *
+ * @param userId the specified user id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByUserId(final String userId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Notification.NOTIFICATION_USER_ID, FilterOperator.EQUAL, userId)));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/OperationRepository.java b/src/main/java/org/b3log/symphony/repository/OperationRepository.java
new file mode 100644
index 000000000..f3bb55ad2
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/OperationRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Operation;
+
+/**
+ * Operation repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Nov 19, 2018
+ * @since 3.4.4
+ */
+@Repository
+public class OperationRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public OperationRepository() {
+ super(Operation.OPERATION);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/OptionRepository.java b/src/main/java/org/b3log/symphony/repository/OptionRepository.java
new file mode 100644
index 000000000..875af2460
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/OptionRepository.java
@@ -0,0 +1,83 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.RepositoryException;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.cache.OptionCache;
+import org.b3log.symphony.model.Option;
+import org.json.JSONObject;
+
+/**
+ * Option repository.
+ *
+ * @author Liang Ding
+ * @version 1.2.1.1, Jun 6, 2019
+ * @since 0.2.0
+ */
+@Repository
+public class OptionRepository extends AbstractRepository {
+
+ /**
+ * Option cache.
+ */
+ @Inject
+ private OptionCache optionCache;
+
+ /**
+ * Public constructor.
+ */
+ public OptionRepository() {
+ super(Option.OPTION);
+ }
+
+ @Override
+ public void remove(final String id) throws RepositoryException {
+ super.remove(id);
+
+ optionCache.removeOption(id);
+ }
+
+ @Override
+ public JSONObject get(final String id) throws RepositoryException {
+ JSONObject ret = optionCache.getOption(id);
+ if (null != ret) {
+ return ret;
+ }
+
+ ret = super.get(id);
+ if (null == ret) {
+ return null;
+ }
+
+ optionCache.putOption(ret);
+
+ return ret;
+ }
+
+ @Override
+ public void update(final String id, final JSONObject option, final String... propertyNames) throws RepositoryException {
+ super.update(id, option, propertyNames);
+
+ option.put(Keys.OBJECT_ID, id);
+ optionCache.putOption(option);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/PermissionRepository.java b/src/main/java/org/b3log/symphony/repository/PermissionRepository.java
new file mode 100644
index 000000000..2487363d7
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/PermissionRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Permission;
+
+/**
+ * Permission repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Dec 3, 2016
+ * @since 1.8.0
+ */
+@Repository
+public class PermissionRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public PermissionRepository() {
+ super(Permission.PERMISSION);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/PointtransferRepository.java b/src/main/java/org/b3log/symphony/repository/PointtransferRepository.java
new file mode 100644
index 000000000..d4f1d9cd9
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/PointtransferRepository.java
@@ -0,0 +1,84 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Pointtransfer;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * Pointtransfer repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.1.0, Dec 12, 2016
+ * @since 1.3.0
+ */
+@Repository
+public class PointtransferRepository extends AbstractRepository {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(PointtransferRepository.class);
+
+ /**
+ * Gets average point of activity eating snake of a user specified by the given user id.
+ *
+ * @param userId the given user id
+ * @return average point, if the point small than {@code 1}, returns {@code pointActivityEatingSnake} which
+ * configured in sym.properties
+ */
+ public int getActivityEatingSnakeAvg(final String userId) {
+ int ret = Pointtransfer.TRANSFER_SUM_C_ACTIVITY_EATINGSNAKE;
+
+ try {
+ final List result = select("SELECT\n"
+ + " AVG(sum) AS point\n"
+ + "FROM\n"
+ + " `" + getName() + "`\n"
+ + "WHERE\n"
+ + " type = 27\n"
+ + "AND toId = ?\n"
+ + "", userId);
+ if (!result.isEmpty()) {
+ ret = result.get(0).optInt(Common.POINT, ret);
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Calc avg point failed", e);
+ }
+
+ if (ret < 1) {
+ ret = Pointtransfer.TRANSFER_SUM_C_ACTIVITY_EATINGSNAKE;
+ }
+
+ return ret;
+ }
+
+ /**
+ * Public constructor.
+ */
+ public PointtransferRepository() {
+ super(Pointtransfer.POINTTRANSFER);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/ReferralRepository.java b/src/main/java/org/b3log/symphony/repository/ReferralRepository.java
new file mode 100644
index 000000000..111cb6b10
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/ReferralRepository.java
@@ -0,0 +1,65 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Referral;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * Referral repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Apr 28, 2016
+ * @since 1.4.0
+ */
+@Repository
+public class ReferralRepository extends AbstractRepository {
+
+ /**
+ * Gets a referral by the specified data id and IP.
+ *
+ * @param dataId the specified data id
+ * @param ip the specified IP
+ * @return referral, returns {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByDataIdAndIP(final String dataId, final String ip) throws RepositoryException {
+ final Query query = new Query().setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(Referral.REFERRAL_DATA_ID, FilterOperator.EQUAL, dataId),
+ new PropertyFilter(Referral.REFERRAL_IP, FilterOperator.EQUAL, ip)
+ )).setPageCount(1).setPage(1, 1);
+
+ final JSONArray records = get(query).optJSONArray(Keys.RESULTS);
+ if (records.length() < 1) {
+ return null;
+ }
+
+ return records.optJSONObject(0);
+ }
+
+ /**
+ * Public constructor.
+ */
+ public ReferralRepository() {
+ super(Referral.REFERRAL);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/ReportRepository.java b/src/main/java/org/b3log/symphony/repository/ReportRepository.java
new file mode 100644
index 000000000..a8d897b25
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/ReportRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Report;
+
+/**
+ * Report repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jun 25, 2018
+ * @since 3.1.0
+ */
+@Repository
+public class ReportRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public ReportRepository() {
+ super(Report.REPORT);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/RevisionRepository.java b/src/main/java/org/b3log/symphony/repository/RevisionRepository.java
new file mode 100644
index 000000000..931583b44
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/RevisionRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Revision;
+
+/**
+ * Revision repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Apr 20, 2016
+ * @since 1.4.0
+ */
+@Repository
+public class RevisionRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public RevisionRepository() {
+ super(Revision.REVISION);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/RewardRepository.java b/src/main/java/org/b3log/symphony/repository/RewardRepository.java
new file mode 100644
index 000000000..82ecaa057
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/RewardRepository.java
@@ -0,0 +1,51 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Reward;
+
+/**
+ * Reward repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Sep 15, 2018
+ * @since 1.3.0
+ */
+@Repository
+public class RewardRepository extends AbstractRepository {
+
+ /**
+ * Removes rewards by the specified data id.
+ *
+ * @param dataId the specified data id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByDataId(final String dataId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Reward.DATA_ID, FilterOperator.EQUAL, dataId)).
+ setPageCount(1));
+ }
+
+ /**
+ * Public constructor.
+ */
+ public RewardRepository() {
+ super(Reward.REWARD);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/RolePermissionRepository.java b/src/main/java/org/b3log/symphony/repository/RolePermissionRepository.java
new file mode 100644
index 000000000..7cff089a7
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/RolePermissionRepository.java
@@ -0,0 +1,79 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Permission;
+import org.b3log.symphony.model.Role;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * Role-Permission repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Aug 27, 2018
+ * @since 1.8.0
+ */
+@Repository
+public class RolePermissionRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public RolePermissionRepository() {
+ super(Role.ROLE + "_" + Permission.PERMISSION);
+ }
+
+ /**
+ * Removes role-permission relations by the specified role id.
+ *
+ * @param roleId the specified role id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByRoleId(final String roleId) throws RepositoryException {
+ final List toRemoves = getByRoleId(roleId);
+ for (final JSONObject toRemove : toRemoves) {
+ remove(toRemove.optString(Keys.OBJECT_ID));
+ }
+ }
+
+ /**
+ * Gets role-permission relations by the specified role id.
+ *
+ * @param roleId the specified role id
+ * @return for example
+ * [{
+ * "oId": "",
+ * "roleId": roleId,
+ * "permissionId": ""
+ * }, ....], returns an empty list if not found
+ *
+ * @throws RepositoryException repository exception
+ */
+ public List getByRoleId(final String roleId) throws RepositoryException {
+ final Query query = new Query().setFilter(
+ new PropertyFilter(Role.ROLE_ID, FilterOperator.EQUAL, roleId)).
+ setPageCount(1);
+
+ return getList(query);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/RoleRepository.java b/src/main/java/org/b3log/symphony/repository/RoleRepository.java
new file mode 100644
index 000000000..883860ecb
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/RoleRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Role;
+
+/**
+ * Role repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Dec 3, 2016
+ * @since 1.8.0
+ */
+@Repository
+public class RoleRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public RoleRepository() {
+ super(Role.ROLE);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/TagArticleRepository.java b/src/main/java/org/b3log/symphony/repository/TagArticleRepository.java
new file mode 100644
index 000000000..f6d3aab0b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/TagArticleRepository.java
@@ -0,0 +1,109 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Article;
+import org.b3log.symphony.model.Tag;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * Tag-Article relation repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.1, Aug 27, 2018
+ * @since 0.2.0
+ */
+@Repository
+public class TagArticleRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public TagArticleRepository() {
+ super(Tag.TAG + "_" + Article.ARTICLE);
+ }
+
+ /**
+ * Removes tag-articles relations by the specified article id.
+ *
+ * @param articleId the specified article id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByArticleId(final String articleId) throws RepositoryException {
+ final List relations = getByArticleId(articleId);
+ for (final JSONObject relation : relations) {
+ remove(relation.optString(Keys.OBJECT_ID));
+ }
+ }
+
+ /**
+ * Gets tag-article relations by the specified article id.
+ *
+ * @param articleId the specified article id
+ * @return for example
+ * [{
+ * "oId": "",
+ * "tag_oId": "",
+ * "article_oId": articleId
+ * }, ....], returns an empty list if not found
+ *
+ * @throws RepositoryException repository exception
+ */
+ public List getByArticleId(final String articleId) throws RepositoryException {
+ final Query query = new Query().setFilter(
+ new PropertyFilter(Article.ARTICLE + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, articleId)).
+ setPageCount(1);
+
+ return getList(query);
+ }
+
+ /**
+ * Gets tag-article relations by the specified tag id.
+ *
+ * @param tagId the specified tag id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "tag_oId": tagId,
+ * "article_oId": ""
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTagId(final String tagId, final int currentPageNum, final int pageSize)
+ throws RepositoryException {
+ final Query query = new Query().
+ setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)).
+ addSort(Article.ARTICLE + "_" + Keys.OBJECT_ID, SortDirection.DESCENDING).
+ setPage(currentPageNum, pageSize).setPageCount(1);
+
+ return get(query);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/TagRepository.java b/src/main/java/org/b3log/symphony/repository/TagRepository.java
new file mode 100644
index 000000000..20c188109
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/TagRepository.java
@@ -0,0 +1,196 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.latke.util.URLs;
+import org.b3log.symphony.cache.TagCache;
+import org.b3log.symphony.model.Tag;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tag repository.
+ *
+ * @author Liang Ding
+ * @version 1.2.0.3, Jun 6, 2018
+ * @since 0.2.0
+ */
+@Repository
+public class TagRepository extends AbstractRepository {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(TagRepository.class);
+
+ /**
+ * Tag cache.
+ */
+ @Inject
+ private TagCache tagCache;
+
+ /**
+ * Tag-Article relation repository.
+ */
+ @Inject
+ private TagArticleRepository tagArticleRepository;
+
+ /**
+ * Public constructor.
+ */
+ public TagRepository() {
+ super(Tag.TAG);
+ }
+
+ @Override
+ public String add(final JSONObject tag) throws RepositoryException {
+ final String ret = super.add(tag);
+
+ tagCache.putTag(tag);
+
+ return ret;
+ }
+
+ @Override
+ public void remove(final String id) throws RepositoryException {
+ super.remove(id);
+
+ tagCache.removeTag(id);
+ }
+
+ @Override
+ public void update(final String id, final JSONObject article, final String... propertyNames) throws RepositoryException {
+ super.update(id, article, propertyNames);
+
+ article.put(Keys.OBJECT_ID, id);
+ tagCache.putTag(article);
+ }
+
+ @Override
+ public JSONObject get(final String id) throws RepositoryException {
+ JSONObject ret = tagCache.getTag(id);
+ if (null != ret) {
+ return ret;
+ }
+
+ ret = super.get(id);
+
+ if (null == ret) {
+ return null;
+ }
+
+ tagCache.putTag(ret);
+
+ return ret;
+ }
+
+ /**
+ * Gets a tag URI with the specified tag title.
+ *
+ * @param title the specified tag title
+ * @return tag URI, returns {@code null} if not found
+ */
+ public String getURIByTitle(final String title) {
+ return tagCache.getURIByTitle(title);
+ }
+
+ /**
+ * Gets a tag by the specified tag URI.
+ *
+ * @param tagURI the specified tag URI
+ * @return a tag, {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByURI(final String tagURI) throws RepositoryException {
+ final String uri = URLs.encode(tagURI);
+ final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG_URI, FilterOperator.EQUAL, uri))
+ .addSort(Tag.TAG_REFERENCE_CNT, SortDirection.DESCENDING)
+ .setPageCount(1);
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+
+ /**
+ * Gets a tag by the specified tag title.
+ *
+ * @param tagTitle the specified tag title
+ * @return a tag, {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTitle(final String tagTitle) throws RepositoryException {
+ final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG_TITLE, FilterOperator.EQUAL, tagTitle)).setPageCount(1);
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+
+ /**
+ * Gets most used tags with the specified number.
+ *
+ * @param num the specified number
+ * @return a list of most used tags, returns an empty list if not found
+ * @throws RepositoryException repository exception
+ */
+ public List getMostUsedTags(final int num) throws RepositoryException {
+ final Query query = new Query().addSort(Tag.TAG_REFERENCE_CNT, SortDirection.DESCENDING).
+ setPage(1, num).setPageCount(1);
+
+ return getList(query);
+ }
+
+ /**
+ * Gets tags of an article specified by the article id.
+ *
+ * @param articleId the specified article id
+ * @return a list of tags of the specified article, returns an empty list if not found
+ * @throws RepositoryException repository exception
+ */
+ public List getByArticleId(final String articleId) throws RepositoryException {
+ final List ret = new ArrayList<>();
+
+ final List tagArticleRelations = tagArticleRepository.getByArticleId(articleId);
+ for (final JSONObject tagArticleRelation : tagArticleRelations) {
+ final String tagId = tagArticleRelation.optString(Tag.TAG + "_" + Keys.OBJECT_ID);
+ final JSONObject tag = get(tagId);
+
+ ret.add(tag);
+ }
+
+ return ret;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/TagTagRepository.java b/src/main/java/org/b3log/symphony/repository/TagTagRepository.java
new file mode 100644
index 000000000..1288c6dca
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/TagTagRepository.java
@@ -0,0 +1,147 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Tag;
+import org.b3log.symphony.util.Symphonys;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tag-Tag relation repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.1, Oct 19, 2016
+ * @since 1.3.0
+ */
+@Repository
+public class TagTagRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public TagTagRepository() {
+ super(Tag.TAG + "_" + Tag.TAG);
+ }
+
+ /**
+ * Gets tag-tag relations by the specified tag1 id.
+ *
+ * @param tag1Id the specified tag1 id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "tag1_oId": tag1Id,
+ * "tag2_oId": "",
+ * "weight": int
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTag1Id(final String tag1Id, final int currentPageNum, final int pageSize)
+ throws RepositoryException {
+ final List filters = new ArrayList<>();
+ filters.add(new PropertyFilter(Tag.TAG + "1_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tag1Id));
+ filters.add(new PropertyFilter(Common.WEIGHT, FilterOperator.GREATER_THAN_OR_EQUAL, Symphonys.TAG_RELATED_WEIGHT));
+
+ final Query query = new Query().setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters)).
+ setPage(currentPageNum, pageSize).setPageCount(1).
+ addSort(Common.WEIGHT, SortDirection.DESCENDING);
+
+ return get(query);
+ }
+
+ /**
+ * Gets tag-tag relations by the specified tag2 id.
+ *
+ * @param tag2Id the specified tag2 id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "tag1_oId": "",
+ * "tag2_oId": tag2Id,
+ * "weight": int
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTag2Id(final String tag2Id, final int currentPageNum, final int pageSize)
+ throws RepositoryException {
+ final List filters = new ArrayList<>();
+ filters.add(new PropertyFilter(Tag.TAG + "2_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tag2Id));
+ filters.add(new PropertyFilter(Common.WEIGHT, FilterOperator.GREATER_THAN_OR_EQUAL, Symphonys.TAG_RELATED_WEIGHT));
+
+ final Query query = new Query().setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters)).
+ setPage(currentPageNum, pageSize).setPageCount(1).
+ addSort(Common.WEIGHT, SortDirection.DESCENDING);
+
+ return get(query);
+ }
+
+ /**
+ * Gets a tag-tag relation by the specified tag1 id and tag2 id.
+ *
+ * @param tag1Id the specified tag1 id
+ * @param tag2Id the specified tag2 id
+ * @return for example
+ * {
+ * "oId": "",
+ * "tag1_oId": tag1Id,
+ * "tag2_oId": tag2Id,
+ * "weight": int
+ * }, returns {@code null} if not found
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTag1IdAndTag2Id(final String tag1Id, final String tag2Id)
+ throws RepositoryException {
+ final List filters = new ArrayList<>();
+ filters.add(new PropertyFilter(Tag.TAG + "1_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tag1Id));
+ filters.add(new PropertyFilter(Tag.TAG + "2_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tag2Id));
+
+ final Query query = new Query().setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters));
+
+ final JSONArray result = get(query).optJSONArray(Keys.RESULTS);
+ if (result.length() < 1) {
+ return null;
+ }
+
+ return result.optJSONObject(0);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/UserRepository.java b/src/main/java/org/b3log/symphony/repository/UserRepository.java
new file mode 100644
index 000000000..a6ecab84b
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/UserRepository.java
@@ -0,0 +1,155 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.model.User;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.cache.UserCache;
+import org.b3log.symphony.model.Role;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * User repository.
+ *
+ * @author Liang Ding
+ * @version 2.1.2.3, Jun 6, 2019
+ * @since 0.2.0
+ */
+@Repository
+public class UserRepository extends AbstractRepository {
+
+ /**
+ * User cache.
+ */
+ @Inject
+ private UserCache userCache;
+
+ /**
+ * Public constructor.
+ */
+ public UserRepository() {
+ super(User.USER);
+ }
+
+ @Override
+ public JSONObject get(final String id) throws RepositoryException {
+ JSONObject ret = userCache.getUser(id);
+ if (null != ret) {
+ return ret;
+ }
+
+ ret = super.get(id);
+
+ if (null == ret) {
+ return null;
+ }
+
+ userCache.putUser(ret);
+
+ return ret;
+ }
+
+ @Override
+ public void update(final String id, final JSONObject user, final String... propertyNames) throws RepositoryException {
+ final JSONObject old = get(id);
+ if (null == old) {
+ return;
+ }
+
+ userCache.removeUser(old);
+ super.update(id, user, propertyNames);
+ user.put(Keys.OBJECT_ID, id);
+ userCache.putUser(user);
+ }
+
+ /**
+ * Gets a user by the specified name.
+ *
+ * @param name the specified name
+ * @return user, returns {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByName(final String name) throws RepositoryException {
+ JSONObject ret = userCache.getUserByName(name);
+ if (null != ret) {
+ return ret;
+ }
+
+ final Query query = new Query().setPageCount(1);
+ query.setFilter(new PropertyFilter(User.USER_NAME, FilterOperator.EQUAL, name));
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ ret = array.optJSONObject(0);
+
+ userCache.putUser(ret);
+
+ return ret;
+ }
+
+ /**
+ * Gets a user by the specified email.
+ *
+ * @param email the specified email
+ * @return user, returns {@code null} if not found
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByEmail(final String email) throws RepositoryException {
+ final Query query = new Query().setPageCount(1);
+ query.setFilter(new PropertyFilter(User.USER_EMAIL, FilterOperator.EQUAL, email.toLowerCase().trim()));
+
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+
+ if (0 == array.length()) {
+ return null;
+ }
+
+ return array.optJSONObject(0);
+ }
+
+ /**
+ * Gets the administrators.
+ *
+ * @return administrators, returns an empty list if not found or error
+ * @throws RepositoryException repository exception
+ */
+ public List getAdmins() throws RepositoryException {
+ List ret = userCache.getAdmins();
+ if (ret.isEmpty()) {
+ final Query query = new Query().setFilter(
+ new PropertyFilter(User.USER_ROLE, FilterOperator.EQUAL, Role.ROLE_ID_C_ADMIN)).setPageCount(1)
+ .addSort(Keys.OBJECT_ID, SortDirection.ASCENDING);
+ ret = getList(query);
+ userCache.putAdmins(ret);
+ }
+
+ return ret;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/UserRoleRepository.java b/src/main/java/org/b3log/symphony/repository/UserRoleRepository.java
new file mode 100644
index 000000000..0744c5d9e
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/UserRoleRepository.java
@@ -0,0 +1,41 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.model.User;
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Role;
+
+/**
+ * User-Role repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Dec 3, 2016
+ * @since 1.8.0
+ */
+@Repository
+public class UserRoleRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public UserRoleRepository() {
+ super(User.USER + "_" + Role.ROLE);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/UserTagRepository.java b/src/main/java/org/b3log/symphony/repository/UserTagRepository.java
new file mode 100644
index 000000000..b47b5885c
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/UserTagRepository.java
@@ -0,0 +1,123 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.model.User;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Common;
+import org.b3log.symphony.model.Tag;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+/**
+ * User-Tag relation repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Apr 17, 2017
+ * @since 0.2.0
+ */
+@Repository
+public class UserTagRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public UserTagRepository() {
+ super(User.USER + "_" + Tag.TAG);
+ }
+
+ /**
+ * Removes user-tag relations by the specified user id and tag id.
+ *
+ * @param userId the specified user id
+ * @param tagId the specified tag id
+ * @param type the specified type
+ * @throws RepositoryException repository exception
+ */
+ public void removeByUserIdAndTagId(final String userId, final String tagId, final int type) throws RepositoryException {
+ final Query query = new Query().setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(User.USER + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, userId),
+ new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId),
+ new PropertyFilter(Common.TYPE, FilterOperator.EQUAL, type)
+ )).setPage(1, Integer.MAX_VALUE).setPageCount(1);
+
+ final JSONArray rels = get(query).optJSONArray(Keys.RESULTS);
+ for (int i = 0; i < rels.length(); i++) {
+ final String id = rels.optJSONObject(i).optString(Keys.OBJECT_ID);
+ remove(id);
+ }
+ }
+
+ /**
+ * Gets user-tag relations by the specified user id.
+ *
+ * @param userId the specified user id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "tag_oId": "",
+ * "user_oId": userId,
+ * "type": "" // "creator"/"article"/"comment", a tag 'creator' is also an 'article' quoter
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByUserId(final String userId, final int currentPageNum, final int pageSize) throws RepositoryException {
+ final Query query = new Query().setFilter(new PropertyFilter(User.USER + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, userId)).
+ setPage(currentPageNum, pageSize).setPageCount(1);
+
+ return get(query);
+ }
+
+ /**
+ * Gets user-tag relations by the specified tag id.
+ *
+ * @param tagId the specified tag id
+ * @param currentPageNum the specified current page number, MUST greater then {@code 0}
+ * @param pageSize the specified page size(count of a page contains objects), MUST greater then {@code 0}
+ * @return for example
+ * {
+ * "pagination": {
+ * "paginationPageCount": 88250
+ * },
+ * "rslts": [{
+ * "oId": "",
+ * "tag_oId": "",
+ * "user_oId": userId,
+ * "type": "" // "creator"/"article"/"comment", a tag 'creator' is also an 'article' quoter
+ * }, ....]
+ * }
+ *
+ * @throws RepositoryException repository exception
+ */
+ public JSONObject getByTagId(final String tagId, final int currentPageNum, final int pageSize) throws RepositoryException {
+ final Query query = new Query().setFilter(new PropertyFilter(Tag.TAG + "_" + Keys.OBJECT_ID, FilterOperator.EQUAL, tagId)).
+ setPage(currentPageNum, pageSize).setPageCount(1);
+
+ return get(query);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/VerifycodeRepository.java b/src/main/java/org/b3log/symphony/repository/VerifycodeRepository.java
new file mode 100644
index 000000000..78a51c863
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/VerifycodeRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.AbstractRepository;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Verifycode;
+
+/**
+ * Verifycode repository.
+ *
+ * @author Liang Ding
+ * @version 1.0.0.0, Jul 3, 2015
+ * @since 1.3.0
+ */
+@Repository
+public class VerifycodeRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public VerifycodeRepository() {
+ super(Verifycode.VERIFYCODE);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/VisitRepository.java b/src/main/java/org/b3log/symphony/repository/VisitRepository.java
new file mode 100644
index 000000000..315fe9ab2
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/VisitRepository.java
@@ -0,0 +1,50 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Visit;
+
+/**
+ * Visit repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Feb 27, 2019
+ * @since 3.2.0
+ */
+@Repository
+public class VisitRepository extends AbstractRepository {
+
+ /**
+ * Public constructor.
+ */
+ public VisitRepository() {
+ super(Visit.VISIT);
+ }
+
+ /**
+ * Remove visits by the specified user id.
+ *
+ * @param userId the specified user id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByUserId(final String userId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Visit.VISIT_USER_ID, FilterOperator.EQUAL, userId)));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/repository/VoteRepository.java b/src/main/java/org/b3log/symphony/repository/VoteRepository.java
new file mode 100644
index 000000000..b70cba494
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/repository/VoteRepository.java
@@ -0,0 +1,84 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.repository;
+
+import org.b3log.latke.Keys;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.repository.annotation.Repository;
+import org.b3log.symphony.model.Vote;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Vote repository.
+ *
+ * @author Liang Ding
+ * @version 1.1.0.0, Sep 15, 2018
+ * @since 1.3.0
+ */
+@Repository
+public class VoteRepository extends AbstractRepository {
+
+ /**
+ * Removes votes by the specified data id.
+ *
+ * @param dataId the specified data id
+ * @throws RepositoryException repository exception
+ */
+ public void removeByDataId(final String dataId) throws RepositoryException {
+ remove(new Query().setFilter(new PropertyFilter(Vote.DATA_ID, FilterOperator.EQUAL, dataId)).
+ setPageCount(1));
+ }
+
+ /**
+ * Removes vote if it exists.
+ *
+ * @param userId the specified user id
+ * @param dataId the specified data entity id
+ * @param dataType the specified data type
+ * @return the removed vote type, returns {@code -1} if removed nothing
+ * @throws RepositoryException repository exception
+ */
+ public int removeIfExists(final String userId, final String dataId, final int dataType) throws RepositoryException {
+ final List filters = new ArrayList<>();
+ filters.add(new PropertyFilter(Vote.USER_ID, FilterOperator.EQUAL, userId));
+ filters.add(new PropertyFilter(Vote.DATA_ID, FilterOperator.EQUAL, dataId));
+ filters.add(new PropertyFilter(Vote.DATA_TYPE, FilterOperator.EQUAL, dataType));
+ final Query query = new Query().setFilter(new CompositeFilter(CompositeFilterOperator.AND, filters));
+ final JSONObject result = get(query);
+ final JSONArray array = result.optJSONArray(Keys.RESULTS);
+ if (0 == array.length()) {
+ return -1;
+ }
+
+ final JSONObject voteToRemove = array.optJSONObject(0);
+ remove(voteToRemove.optString(Keys.OBJECT_ID));
+
+ return voteToRemove.optInt(Vote.TYPE);
+ }
+
+ /**
+ * Public constructor.
+ */
+ public VoteRepository() {
+ super(Vote.VOTE);
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/service/ActivityMgmtService.java b/src/main/java/org/b3log/symphony/service/ActivityMgmtService.java
new file mode 100644
index 000000000..4c08d74df
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/service/ActivityMgmtService.java
@@ -0,0 +1,579 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.service;
+
+import jodd.util.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.time.DateFormatUtils;
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.*;
+import org.b3log.latke.service.LangPropsService;
+import org.b3log.latke.service.annotation.Service;
+import org.b3log.symphony.model.Liveness;
+import org.b3log.symphony.model.Pointtransfer;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.repository.CharacterRepository;
+import org.b3log.symphony.repository.PointtransferRepository;
+import org.b3log.symphony.util.Symphonys;
+import org.b3log.symphony.util.Tesseracts;
+import org.json.JSONObject;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Activity management service.
+ *
+ * @author Liang Ding
+ * @author Zephyr
+ * @version 1.6.10.2, Jun 8, 2019
+ * @since 1.3.0
+ */
+@Service
+public class ActivityMgmtService {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ActivityMgmtService.class);
+
+ /**
+ * Character repository.
+ */
+ @Inject
+ private CharacterRepository characterRepository;
+
+ /**
+ * Pointtransfer repository.
+ */
+ @Inject
+ private PointtransferRepository pointtransferRepository;
+
+ /**
+ * Pointtransfer query service.
+ */
+ @Inject
+ private PointtransferQueryService pointtransferQueryService;
+
+ /**
+ * Pointtransfer management service.
+ */
+ @Inject
+ private PointtransferMgmtService pointtransferMgmtService;
+
+ /**
+ * Activity query service.
+ */
+ @Inject
+ private ActivityQueryService activityQueryService;
+
+ /**
+ * User management service.
+ */
+ @Inject
+ private UserMgmtService userMgmtService;
+
+ /**
+ * User query service.
+ */
+ @Inject
+ private UserQueryService userQueryService;
+
+ /**
+ * Language service.
+ */
+ @Inject
+ private LangPropsService langPropsService;
+
+ /**
+ * Liveness management service.
+ */
+ @Inject
+ private LivenessMgmtService livenessMgmtService;
+
+ /**
+ * Liveness query service.
+ */
+ @Inject
+ private LivenessQueryService livenessQueryService;
+
+ /**
+ * Starts eating snake.
+ *
+ * @param userId the specified user id
+ * @return result
+ */
+ public synchronized JSONObject startEatingSnake(final String userId) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+
+ final int startPoint = pointtransferRepository.getActivityEatingSnakeAvg(userId);
+
+ final boolean succ = null != pointtransferMgmtService.transfer(userId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_EATINGSNAKE,
+ startPoint, "", System.currentTimeMillis(), "");
+
+ ret.put(Keys.STATUS_CODE, succ);
+
+ final String msg = succ ? "started" : langPropsService.get("activityStartEatingSnakeFailLabel");
+ ret.put(Keys.MSG, msg);
+
+ livenessMgmtService.incLiveness(userId, Liveness.LIVENESS_ACTIVITY);
+
+ return ret;
+ }
+
+ /**
+ * Collects eating snake.
+ *
+ * @param userId the specified user id
+ * @param score the specified score
+ * @return result
+ */
+ public synchronized JSONObject collectEatingSnake(final String userId, final int score) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+
+ if (score < 1) {
+ ret.put(Keys.STATUS_CODE, true);
+
+ return ret;
+ }
+
+ final int max = Symphonys.POINT_ACTIVITY_EATINGSNAKE_COLLECT_MAX;
+ final int amout = score > max ? max : score;
+
+ final boolean succ = null != pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_EATINGSNAKE_COLLECT, amout,
+ "", System.currentTimeMillis(), "");
+
+ if (!succ) {
+ ret.put(Keys.MSG, "Sorry, transfer point failed, please contact admin");
+ }
+
+ ret.put(Keys.STATUS_CODE, succ);
+
+ return ret;
+ }
+
+ /**
+ * Submits the specified character to recognize.
+ *
+ * @param userId the specified user id
+ * @param characterImg the specified character image encoded by Base64
+ * @param character the specified character
+ * @return recognition result
+ */
+ public synchronized JSONObject submitCharacter(final String userId, final String characterImg, final String character) {
+ String recongnizeFailedMsg = langPropsService.get("activityCharacterRecognizeFailedLabel");
+
+ final JSONObject ret = new JSONObject();
+ ret.put(Keys.STATUS_CODE, false);
+ ret.put(Keys.MSG, recongnizeFailedMsg);
+
+ if (StringUtils.isBlank(characterImg) || StringUtils.isBlank(character)) {
+ ret.put(Keys.STATUS_CODE, false);
+ ret.put(Keys.MSG, recongnizeFailedMsg);
+
+ return ret;
+ }
+
+ final byte[] data = Base64.decode(characterImg);
+ OutputStream stream = null;
+ final String tmpDir = System.getProperty("java.io.tmpdir");
+ final String imagePath = tmpDir + "/" + userId + "-character.png";
+
+ try {
+ stream = new FileOutputStream(imagePath);
+ stream.write(data);
+ stream.flush();
+ stream.close();
+ } catch (final IOException e) {
+ LOGGER.log(Level.ERROR, "Submits character failed", e);
+
+ return ret;
+ } finally {
+ if (null != stream) {
+ try {
+ stream.close();
+ } catch (final IOException ex) {
+ LOGGER.log(Level.ERROR, "Closes stream failed", ex);
+ }
+ }
+ }
+
+ final String recognizedCharacter = Tesseracts.recognizeCharacter(imagePath);
+ LOGGER.info("Character [" + character + "], recognized [" + recognizedCharacter + "], image path [" + imagePath
+ + "]");
+ if (StringUtils.equals(character, recognizedCharacter)) {
+ final Query query = new Query();
+ query.setFilter(CompositeFilterOperator.and(
+ new PropertyFilter(org.b3log.symphony.model.Character.CHARACTER_USER_ID, FilterOperator.EQUAL, userId),
+ new PropertyFilter(org.b3log.symphony.model.Character.CHARACTER_CONTENT, FilterOperator.EQUAL, character)
+ ));
+
+ try {
+ if (characterRepository.count(query) > 0) {
+ return ret;
+ }
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Count characters failed [userId=" + userId + ", character=" + character + "]", e);
+
+ return ret;
+ }
+
+ final JSONObject record = new JSONObject();
+ record.put(org.b3log.symphony.model.Character.CHARACTER_CONTENT, character);
+ record.put(org.b3log.symphony.model.Character.CHARACTER_IMG, characterImg);
+ record.put(org.b3log.symphony.model.Character.CHARACTER_USER_ID, userId);
+
+ String characterId;
+ final Transaction transaction = characterRepository.beginTransaction();
+ try {
+ characterId = characterRepository.add(record);
+
+ transaction.commit();
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Submits character failed", e);
+
+ if (null != transaction) {
+ transaction.rollback();
+ }
+
+ return ret;
+ }
+
+ pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_CHARACTER, Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHARACTER,
+ characterId, System.currentTimeMillis(), "");
+
+ ret.put(Keys.STATUS_CODE, true);
+ ret.put(Keys.MSG, langPropsService.get("activityCharacterRecognizeSuccLabel"));
+ } else {
+ recongnizeFailedMsg = recongnizeFailedMsg.replace("{一}", recognizedCharacter);
+ ret.put(Keys.STATUS_CODE, false);
+ ret.put(Keys.MSG, recongnizeFailedMsg);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Daily checkin.
+ *
+ * @param userId the specified user id
+ * @return {@code Random int} if checkin succeeded, returns {@code Integer.MIN_VALUE} otherwise
+ */
+ public synchronized int dailyCheckin(final String userId) {
+ if (activityQueryService.isCheckedinToday(userId)) {
+ return Integer.MIN_VALUE;
+ }
+
+ final Random random = new Random();
+ final int sum = random.nextInt(Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKIN_MAX)
+ % (Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKIN_MAX - Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKIN_MIN + 1)
+ + Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKIN_MIN;
+ final boolean succ = null != pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_CHECKIN, sum, userId, System.currentTimeMillis(), "");
+ if (!succ) {
+ return Integer.MIN_VALUE;
+ }
+
+ try {
+ final JSONObject user = userQueryService.getUser(userId);
+
+ int currentStreakStart = user.optInt(UserExt.USER_CURRENT_CHECKIN_STREAK_START);
+ int currentStreakEnd = user.optInt(UserExt.USER_CURRENT_CHECKIN_STREAK_END);
+
+ final Date today = new Date();
+ user.put(UserExt.USER_CHECKIN_TIME, today.getTime());
+
+ final String datePattern = "yyyyMMdd";
+ final String todayStr = DateFormatUtils.format(today, datePattern);
+ final int todayInt = Integer.valueOf(todayStr);
+
+ if (0 == currentStreakStart) {
+ user.put(UserExt.USER_CURRENT_CHECKIN_STREAK_START, todayInt);
+ user.put(UserExt.USER_CURRENT_CHECKIN_STREAK_END, todayInt);
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK_START, todayInt);
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK_END, todayInt);
+ user.put(UserExt.USER_CURRENT_CHECKIN_STREAK, 1);
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK, 1);
+
+ userMgmtService.updateUser(userId, user);
+
+ return sum;
+ }
+
+ final Date endDate = DateUtils.parseDate(String.valueOf(currentStreakEnd), new String[]{datePattern});
+ final Date nextDate = DateUtils.addDays(endDate, 1);
+ if (!DateUtils.isSameDay(nextDate, today)) {
+ user.put(UserExt.USER_CURRENT_CHECKIN_STREAK_START, todayInt);
+ }
+ user.put(UserExt.USER_CURRENT_CHECKIN_STREAK_END, todayInt);
+
+ currentStreakStart = user.optInt(UserExt.USER_CURRENT_CHECKIN_STREAK_START);
+ currentStreakEnd = user.optInt(UserExt.USER_CURRENT_CHECKIN_STREAK_END);
+
+ final Date currentStreakStartDate = DateUtils.parseDate(String.valueOf(currentStreakStart), new String[]{datePattern});
+ final Date currentStreakEndDate = DateUtils.parseDate(String.valueOf(currentStreakEnd), new String[]{datePattern});
+ final int currentStreakDays = (int) ((currentStreakEndDate.getTime() - currentStreakStartDate.getTime()) / 86400000) + 1;
+ user.put(UserExt.USER_CURRENT_CHECKIN_STREAK, currentStreakDays);
+
+ final int longestStreakStart = user.optInt(UserExt.USER_LONGEST_CHECKIN_STREAK_START);
+ final int longestStreakEnd = user.optInt(UserExt.USER_LONGEST_CHECKIN_STREAK_END);
+ final Date longestStreakStartDate = DateUtils.parseDate(String.valueOf(longestStreakStart), new String[]{datePattern});
+ final Date longestStreakEndDate = DateUtils.parseDate(String.valueOf(longestStreakEnd), new String[]{datePattern});
+ final int longestStreakDays = (int) ((longestStreakEndDate.getTime() - longestStreakStartDate.getTime()) / 86400000) + 1;
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK, longestStreakDays);
+
+ if (longestStreakDays < currentStreakDays) {
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK_START, currentStreakStart);
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK_END, currentStreakEnd);
+
+ user.put(UserExt.USER_LONGEST_CHECKIN_STREAK, currentStreakDays);
+ }
+
+ userMgmtService.updateUser(userId, user);
+
+ if (currentStreakDays > 0 && 0 == currentStreakDays % 10) {
+ // Additional Point
+ pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_CHECKIN_STREAK,
+ Pointtransfer.TRANSFER_SUM_C_ACTIVITY_CHECKINT_STREAK, userId, System.currentTimeMillis(), "");
+ }
+
+ livenessMgmtService.incLiveness(userId, Liveness.LIVENESS_ACTIVITY);
+
+ return sum;
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Checkin streak error", e);
+
+ return Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * Bets 1A0001.
+ *
+ * @param userId the specified user id
+ * @param amount the specified amount
+ * @param smallOrLarge the specified small or large
+ * @return result
+ */
+ public synchronized JSONObject bet1A0001(final String userId, final int amount, final int smallOrLarge) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+
+ if (activityQueryService.is1A0001Today(userId)) {
+ ret.put(Keys.MSG, langPropsService.get("activityParticipatedLabel"));
+
+ return ret;
+ }
+
+ final String date = DateFormatUtils.format(new Date(), "yyyyMMdd");
+
+ final boolean succ = null != pointtransferMgmtService.transfer(userId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_1A0001, amount, date + "-" + smallOrLarge, System.currentTimeMillis(), "");
+
+ ret.put(Keys.STATUS_CODE, succ);
+
+ final String msg = succ
+ ? langPropsService.get("activityBetSuccLabel") : langPropsService.get("activityBetFailLabel");
+ ret.put(Keys.MSG, msg);
+
+ livenessMgmtService.incLiveness(userId, Liveness.LIVENESS_ACTIVITY);
+
+ return ret;
+ }
+
+ /**
+ * Collects 1A0001.
+ *
+ * @param userId the specified user id
+ * @return result
+ */
+ public synchronized JSONObject collect1A0001(final String userId) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+
+ if (!activityQueryService.is1A0001Today(userId)) {
+ ret.put(Keys.MSG, langPropsService.get("activityNotParticipatedLabel"));
+
+ return ret;
+ }
+
+ if (activityQueryService.isCollected1A0001Today(userId)) {
+ ret.put(Keys.MSG, langPropsService.get("activityParticipatedLabel"));
+
+ return ret;
+ }
+
+ final List records = pointtransferQueryService.getLatestPointtransfers(userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_1A0001, 1);
+ final JSONObject pointtransfer = records.get(0);
+ final String data = pointtransfer.optString(Pointtransfer.DATA_ID);
+ final String smallOrLarge = data.split("-")[1];
+ final int sum = pointtransfer.optInt(Pointtransfer.SUM);
+
+ String smallOrLargeResult = null;
+ try {
+ final Document doc = Jsoup.parse(new URL("http://stockpage.10jqka.com.cn/1A0001/quote/header/"), 5000);
+ final JSONObject result = new JSONObject(doc.text());
+ final String price = result.optJSONObject("data").optJSONObject("1A0001").optString("10");
+
+ if (!price.contains(".")) {
+ smallOrLargeResult = "0";
+ } else {
+ int endInt = 0;
+ if (price.split("\\.")[1].length() > 1) {
+ final String end = price.substring(price.length() - 1);
+ endInt = Integer.valueOf(end);
+ }
+
+ if (0 <= endInt && endInt <= 4) {
+ smallOrLargeResult = "0";
+ } else if (5 <= endInt && endInt <= 9) {
+ smallOrLargeResult = "1";
+ } else {
+ LOGGER.error("Activity 1A0001 collect result [" + endInt + "]");
+ }
+ }
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Collect 1A0001 failed", e);
+
+ ret.put(Keys.MSG, langPropsService.get("activity1A0001CollectFailLabel"));
+
+ return ret;
+ }
+
+ if (StringUtils.isBlank(smallOrLarge)) {
+ ret.put(Keys.MSG, langPropsService.get("activity1A0001CollectFailLabel"));
+
+ return ret;
+ }
+
+ ret.put(Keys.STATUS_CODE, true);
+ if (StringUtils.equals(smallOrLarge, smallOrLargeResult)) {
+ final int amount = sum * 2;
+
+ final boolean succ = null != pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_1A0001_COLLECT, amount,
+ DateFormatUtils.format(new Date(), "yyyyMMdd") + "-" + smallOrLargeResult, System.currentTimeMillis(), "");
+
+ if (succ) {
+ String msg = langPropsService.get("activity1A0001CollectSucc1Label");
+ msg = msg.replace("{point}", String.valueOf(amount));
+
+ ret.put(Keys.MSG, msg);
+ } else {
+ ret.put(Keys.MSG, langPropsService.get("activity1A0001CollectFailLabel"));
+ }
+ } else {
+ ret.put(Keys.MSG, langPropsService.get("activity1A0001CollectSucc0Label"));
+ }
+
+ return ret;
+ }
+
+ /**
+ * Collects yesterday's liveness reward.
+ *
+ * @param userId the specified user id
+ */
+ public synchronized void yesterdayLivenessReward(final String userId) {
+ if (activityQueryService.isCollectedYesterdayLivenessReward(userId)) {
+ return;
+ }
+
+ final JSONObject yesterdayLiveness = livenessQueryService.getYesterdayLiveness(userId);
+ if (null == yesterdayLiveness) {
+ return;
+ }
+
+ final int sum = Liveness.calcPoint(yesterdayLiveness);
+
+ if (0 == sum) {
+ return;
+ }
+
+ boolean succ = null != pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_YESTERDAY_LIVENESS_REWARD, sum, userId, System.currentTimeMillis(), "");
+ if (!succ) {
+ return;
+ }
+
+ // Today liveness (activity)
+ livenessMgmtService.incLiveness(userId, Liveness.LIVENESS_ACTIVITY);
+ }
+
+ /**
+ * Starts Gobang.
+ *
+ * @param userId the specified user id
+ * @return result
+ */
+ public synchronized JSONObject startGobang(final String userId) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+
+ final int startPoint = Pointtransfer.TRANSFER_SUM_C_ACTIVITY_GOBANG_START;
+
+ final boolean succ = null != pointtransferMgmtService.transfer(userId, Pointtransfer.ID_C_SYS,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_GOBANG,
+ startPoint, "", System.currentTimeMillis(), "");
+
+ ret.put(Keys.STATUS_CODE, succ);
+
+ final String msg = succ ? "started" : langPropsService.get("activityStartGobangFailLabel");
+ ret.put(Keys.MSG, msg);
+
+ livenessMgmtService.incLiveness(userId, Liveness.LIVENESS_ACTIVITY);
+
+ return ret;
+ }
+
+ /**
+ * Collects Gobang.
+ *
+ * @param userId the specified user id
+ * @param score the specified score
+ * @return result
+ */
+ public synchronized JSONObject collectGobang(final String userId, final int score) {
+ final JSONObject ret = new JSONObject().put(Keys.STATUS_CODE, false);
+
+ final boolean succ = null != pointtransferMgmtService.transfer(Pointtransfer.ID_C_SYS, userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_GOBANG_COLLECT, score,
+ "", System.currentTimeMillis(), "");
+
+ if (!succ) {
+ ret.put(Keys.MSG, "Sorry, transfer point failed, please contact admin");
+ }
+
+ ret.put(Keys.STATUS_CODE, succ);
+
+ return ret;
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/service/ActivityQueryService.java b/src/main/java/org/b3log/symphony/service/ActivityQueryService.java
new file mode 100644
index 000000000..feffc7e00
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/service/ActivityQueryService.java
@@ -0,0 +1,294 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.b3log.symphony.service;
+
+import org.apache.commons.lang.time.DateUtils;
+import org.b3log.latke.Keys;
+import org.b3log.latke.ioc.Inject;
+import org.b3log.latke.logging.Level;
+import org.b3log.latke.logging.Logger;
+import org.b3log.latke.repository.Query;
+import org.b3log.latke.repository.RepositoryException;
+import org.b3log.latke.repository.SortDirection;
+import org.b3log.latke.service.annotation.Service;
+import org.b3log.latke.util.CollectionUtils;
+import org.b3log.latke.util.Stopwatchs;
+import org.b3log.symphony.model.Pointtransfer;
+import org.b3log.symphony.model.UserExt;
+import org.b3log.symphony.repository.PointtransferRepository;
+import org.b3log.symphony.repository.UserRepository;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Activity query service.
+ *
+ * @author Liang Ding
+ * @version 1.5.2.0, May 9, 2018
+ * @since 1.3.0
+ */
+@Service
+public class ActivityQueryService {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOGGER = Logger.getLogger(ActivityQueryService.class);
+
+ /**
+ * Pointtransfer repository.
+ */
+ @Inject
+ private PointtransferRepository pointtransferRepository;
+
+ /**
+ * User repository.
+ */
+ @Inject
+ private UserRepository userRepository;
+
+ /**
+ * Pointtransfer query service.
+ */
+ @Inject
+ private PointtransferQueryService pointtransferQueryService;
+
+ /**
+ * Avatar query service.
+ */
+ @Inject
+ private AvatarQueryService avatarQueryService;
+
+ /**
+ * Gets average point of activity eating snake of a user specified by the given user id.
+ *
+ * @param userId the given user id
+ * @return average point, if the point small than {@code 1}, returns {@code pointActivityEatingSnake} which
+ * configured in sym.properties
+ */
+ public int getEatingSnakeAvgPoint(final String userId) {
+ return pointtransferRepository.getActivityEatingSnakeAvg(userId);
+ }
+
+ /**
+ * Gets the top eating snake users (single game max) with the specified fetch size.
+ *
+ * @param fetchSize the specified fetch size
+ * @return users, returns an empty list if not found
+ */
+ public List getTopEatingSnakeUsersMax(final int fetchSize) {
+ final List ret = new ArrayList<>();
+
+ try {
+ final List users = userRepository.select("SELECT\n"
+ + " u.*, MAX(sum) AS point\n"
+ + "FROM\n"
+ + " " + pointtransferRepository.getName() + " AS p,\n"
+ + " " + userRepository.getName() + " AS u\n"
+ + "WHERE\n"
+ + " p.toId = u.oId\n"
+ + "AND type = 27\n"
+ + "GROUP BY\n"
+ + " toId\n"
+ + "ORDER BY\n"
+ + " point DESC\n"
+ + "LIMIT ?", fetchSize);
+
+ for (final JSONObject user : users) {
+ avatarQueryService.fillUserAvatarURL(user);
+
+ ret.add(user);
+ }
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Gets top eating snake users error", e);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Gets the top eating snake users (sum) with the specified fetch size.
+ *
+ * @param fetchSize the specified fetch size
+ * @return users, returns an empty list if not found
+ */
+ public List getTopEatingSnakeUsersSum(final int fetchSize) {
+ final List ret = new ArrayList<>();
+
+ try {
+ final List users = userRepository.select("SELECT\n"
+ + " u.*, Sum(sum) AS point\n"
+ + "FROM\n"
+ + " " + pointtransferRepository.getName() + " AS p,\n"
+ + " " + userRepository.getName() + " AS u\n"
+ + "WHERE\n"
+ + " p.toId = u.oId\n"
+ + "AND type = 27\n"
+ + "GROUP BY\n"
+ + " toId\n"
+ + "ORDER BY\n"
+ + " point DESC\n"
+ + "LIMIT ?", fetchSize);
+
+ for (final JSONObject user : users) {
+ avatarQueryService.fillUserAvatarURL(user);
+
+ ret.add(user);
+ }
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Gets top eating snake users error", e);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Gets the top checkin users with the specified fetch size.
+ *
+ * @param fetchSize the specified fetch size
+ * @return users, returns an empty list if not found
+ */
+ public List getTopCheckinUsers(final int fetchSize) {
+ final List ret = new ArrayList<>();
+
+ final Query query = new Query().addSort(UserExt.USER_LONGEST_CHECKIN_STREAK, SortDirection.DESCENDING).
+ addSort(UserExt.USER_CURRENT_CHECKIN_STREAK, SortDirection.DESCENDING).
+ setPage(1, fetchSize);
+
+ try {
+ final JSONObject result = userRepository.get(query);
+ final List users = CollectionUtils.jsonArrayToList(result.optJSONArray(Keys.RESULTS));
+
+ for (final JSONObject user : users) {
+ if (UserExt.USER_APP_ROLE_C_HACKER == user.optInt(UserExt.USER_APP_ROLE)) {
+ user.put(UserExt.USER_T_POINT_HEX, Integer.toHexString(user.optInt(UserExt.USER_POINT)));
+ } else {
+ user.put(UserExt.USER_T_POINT_CC, UserExt.toCCString(user.optInt(UserExt.USER_POINT)));
+ }
+
+ avatarQueryService.fillUserAvatarURL(user);
+
+ ret.add(user);
+ }
+ } catch (final RepositoryException e) {
+ LOGGER.log(Level.ERROR, "Gets top checkin users error", e);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Does checkin today?
+ *
+ * @param userId the specified user id
+ * @return {@code true} if checkin succeeded, returns {@code false} otherwise
+ */
+ public synchronized boolean isCheckedinToday(final String userId) {
+ Stopwatchs.start("Checks checkin");
+ try {
+ final JSONObject user = userRepository.get(userId);
+ final long time = user.optLong(UserExt.USER_CHECKIN_TIME);
+ if (DateUtils.isSameDay(new Date(), new Date(time))) {
+ return true;
+ }
+
+ // 使用缓存检查在某个竞态条件下会有问题。如果缓存检查没有签到,则再查库判断
+ final List latestPointtransfers = pointtransferQueryService.getLatestPointtransfers(userId, Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_CHECKIN, 1);
+ if (latestPointtransfers.isEmpty()) {
+ return false;
+ }
+
+ final JSONObject latestPointtransfer = latestPointtransfers.get(0);
+ final long checkinTime = latestPointtransfer.optLong(Pointtransfer.TIME);
+
+ return DateUtils.isSameDay(new Date(), new Date(checkinTime));
+ } catch (final Exception e) {
+ LOGGER.log(Level.ERROR, "Checks checkin failed", e);
+
+ return true;
+ } finally {
+ Stopwatchs.end();
+ }
+ }
+
+ /**
+ * Does participate 1A0001 today?
+ *
+ * @param userId the specified user id
+ * @return {@code true} if participated, returns {@code false} otherwise
+ */
+ public synchronized boolean is1A0001Today(final String userId) {
+ final Date now = new Date();
+
+ final List records = pointtransferQueryService.getLatestPointtransfers(userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_1A0001, 1);
+ if (records.isEmpty()) {
+ return false;
+ }
+
+ final JSONObject maybeToday = records.get(0);
+ final long time = maybeToday.optLong(Pointtransfer.TIME);
+
+ return DateUtils.isSameDay(now, new Date(time));
+ }
+
+ /**
+ * Did collect 1A0001 today?
+ *
+ * @param userId the specified user id
+ * @return {@code true} if collected, returns {@code false} otherwise
+ */
+ public synchronized boolean isCollected1A0001Today(final String userId) {
+ final Date now = new Date();
+
+ final List records = pointtransferQueryService.getLatestPointtransfers(userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_1A0001_COLLECT, 1);
+ if (records.isEmpty()) {
+ return false;
+ }
+
+ final JSONObject maybeToday = records.get(0);
+ final long time = maybeToday.optLong(Pointtransfer.TIME);
+
+ return DateUtils.isSameDay(now, new Date(time));
+ }
+
+ /**
+ * Did collect yesterday's liveness reward?
+ *
+ * @param userId the specified user id
+ * @return {@code true} if collected, returns {@code false} otherwise
+ */
+ public synchronized boolean isCollectedYesterdayLivenessReward(final String userId) {
+ final Date now = new Date();
+
+ final List records = pointtransferQueryService.getLatestPointtransfers(userId,
+ Pointtransfer.TRANSFER_TYPE_C_ACTIVITY_YESTERDAY_LIVENESS_REWARD, 1);
+ if (records.isEmpty()) {
+ return false;
+ }
+
+ final JSONObject maybeToday = records.get(0);
+ final long time = maybeToday.optLong(Pointtransfer.TIME);
+
+ return DateUtils.isSameDay(now, new Date(time));
+ }
+}
diff --git a/src/main/java/org/b3log/symphony/service/ArticleMgmtService.java b/src/main/java/org/b3log/symphony/service/ArticleMgmtService.java
new file mode 100644
index 000000000..018ac1470
--- /dev/null
+++ b/src/main/java/org/b3log/symphony/service/ArticleMgmtService.java
@@ -0,0 +1,1818 @@
+/*
+ * Symphony - A modern community (forum/BBS/SNS/blog) platform written in Java.
+ * Copyright (C) 2012-present, b3log.org
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see