Repository: conversationai/conversationai-moderator
Branch: main
Commit: 0373a11e3376
Files: 535
Total size: 15.7 MB
Directory structure:
gitextract_v8p2gl5q/
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── QUICKSTART.md
├── README.md
├── bin/
│ ├── build
│ ├── initdb
│ ├── install
│ ├── install-circleci
│ ├── link-packages
│ ├── lint
│ ├── lint-fix
│ ├── osmod
│ ├── run
│ ├── storybook
│ ├── sync-db
│ ├── test
│ └── watch
├── deployments/
│ ├── gcloud/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── deploy-sql.sh
│ │ ├── deploy.sh
│ │ ├── kubernetes-deployment.yaml
│ │ └── kubernetes-networking.yaml
│ ├── local/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── docker-compose.yml
│ └── standalone/
│ ├── .dockerignore
│ ├── Dockerfile
│ └── initialise_db.sh
├── design-files/
│ ├── Moderator-StickerSheet-20161117-DAS/
│ │ ├── document.json
│ │ ├── meta.json
│ │ ├── pages/
│ │ │ ├── 1B67083D-3430-4865-A36A-6C687A1EEB45.json
│ │ │ └── 226D6615-C16F-4B84-92E0-291B2F1B15C4.json
│ │ └── user.json
│ └── Moderator-StickerSheet-20161117-DAS.sketch
├── docs/
│ ├── auth.md
│ ├── comment_flow.md
│ ├── modeling.md
│ ├── osmod_assistant_protocol.md
│ ├── osmod_services_api.md
│ ├── osmod_task_api.md
│ ├── sql_queries.sql
│ ├── worker.md
│ └── youtube_integration.md
├── lerna.json
├── package.json
├── packages/
│ ├── README.md
│ ├── backend-api/
│ │ ├── .sequelizerc
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── bin/
│ │ │ ├── check_migrations.sh
│ │ │ ├── make_migration.sh
│ │ │ ├── osmod-test.js
│ │ │ ├── osmod.js
│ │ │ ├── run_sequelize_sync.js
│ │ │ └── run_task
│ │ ├── data/
│ │ │ ├── alice.txt
│ │ │ ├── brexit.csv
│ │ │ ├── climate.csv
│ │ │ ├── election.csv
│ │ │ └── wikipedia.csv
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── actions/
│ │ │ │ ├── assignment_updaters.ts
│ │ │ │ └── object_updaters.ts
│ │ │ ├── api/
│ │ │ │ ├── assistant/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── schema.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── router.ts
│ │ │ │ ├── services/
│ │ │ │ │ ├── assignments.ts
│ │ │ │ │ ├── authorCounts.ts
│ │ │ │ │ ├── commentActions.ts
│ │ │ │ │ ├── commentSources.ts
│ │ │ │ │ ├── editComment.ts
│ │ │ │ │ ├── histogramScores/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── util.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── moderatedCounts.ts
│ │ │ │ │ ├── search.ts
│ │ │ │ │ ├── serializer.ts
│ │ │ │ │ ├── simple.ts
│ │ │ │ │ ├── textSizes.ts
│ │ │ │ │ └── updateNotifications.ts
│ │ │ │ └── util/
│ │ │ │ ├── permissions.ts
│ │ │ │ ├── server.ts
│ │ │ │ ├── sortCommentIds.ts
│ │ │ │ └── validation.ts
│ │ │ ├── auth/
│ │ │ │ ├── config.ts
│ │ │ │ ├── providers/
│ │ │ │ │ ├── google.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── jwt.ts
│ │ │ │ ├── router.ts
│ │ │ │ ├── tokens.ts
│ │ │ │ ├── users.ts
│ │ │ │ ├── utils.ts
│ │ │ │ └── youtube.ts
│ │ │ ├── commands/
│ │ │ │ ├── articles/
│ │ │ │ │ └── delete.ts
│ │ │ │ ├── comments/
│ │ │ │ │ ├── calculate_text_size.ts
│ │ │ │ │ ├── data_helpers.ts
│ │ │ │ │ ├── delete.ts
│ │ │ │ │ ├── flag.ts
│ │ │ │ │ ├── generate.ts
│ │ │ │ │ ├── import.ts
│ │ │ │ │ ├── rebuild_reply_relations.ts
│ │ │ │ │ ├── recalculate_text_sizes.ts
│ │ │ │ │ ├── recalculate_top_scores.ts
│ │ │ │ │ ├── rescore.ts
│ │ │ │ │ └── send_to_scorer.ts
│ │ │ │ ├── denormalize.ts
│ │ │ │ ├── tests/
│ │ │ │ │ └── youtube.ts
│ │ │ │ └── users/
│ │ │ │ ├── create.ts
│ │ │ │ └── get_token.ts
│ │ │ ├── config.ts
│ │ │ ├── domain/
│ │ │ │ ├── articles/
│ │ │ │ │ ├── countDenormalization.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── categories/
│ │ │ │ │ ├── countDenormalization.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── commentScores/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── comments/
│ │ │ │ │ ├── countDenormalization.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── textSizes.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── integrations/
│ │ │ │ ├── decisions.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── youtube/
│ │ │ │ ├── actions.ts
│ │ │ │ ├── authenticate.ts
│ │ │ │ ├── channels.ts
│ │ │ │ ├── comments.ts
│ │ │ │ ├── hooks.ts
│ │ │ │ ├── objectmap.ts
│ │ │ │ ├── task.ts
│ │ │ │ └── videos.ts
│ │ │ ├── logger.ts
│ │ │ ├── models/
│ │ │ │ ├── article.ts
│ │ │ │ ├── category.ts
│ │ │ │ ├── comment.ts
│ │ │ │ ├── comment_flag.ts
│ │ │ │ ├── comment_score.ts
│ │ │ │ ├── comment_score_request.ts
│ │ │ │ ├── comment_size.ts
│ │ │ │ ├── comment_summary_score.ts
│ │ │ │ ├── comment_top_score.ts
│ │ │ │ ├── configuration.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── csrf.ts
│ │ │ │ ├── decision.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── moderation_rule.ts
│ │ │ │ ├── moderator_assignment.ts
│ │ │ │ ├── preselect.ts
│ │ │ │ ├── tag.ts
│ │ │ │ ├── tagging_sensitivity.ts
│ │ │ │ ├── user.ts
│ │ │ │ ├── user_category_assignment.ts
│ │ │ │ └── user_social_auth.ts
│ │ │ ├── notification_router.ts
│ │ │ ├── pipeline/
│ │ │ │ ├── apiShim.ts
│ │ │ │ ├── hooks.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── pipeline.ts
│ │ │ │ ├── rules.ts
│ │ │ │ ├── shim.ts
│ │ │ │ └── state.ts
│ │ │ ├── processing/
│ │ │ │ ├── api/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── known_tasks.ts
│ │ │ │ │ └── permissions.ts
│ │ │ │ ├── dashboard.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── tasks/
│ │ │ │ │ ├── comment_actions.ts
│ │ │ │ │ ├── db_operations.ts
│ │ │ │ │ ├── heartbeat.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── process_machine_score.ts
│ │ │ │ │ ├── process_tagging.ts
│ │ │ │ │ ├── score_actions.ts
│ │ │ │ │ ├── score_tag_actions.ts
│ │ │ │ │ └── send_comment_for_scoring.ts
│ │ │ │ ├── util.ts
│ │ │ │ └── worker.ts
│ │ │ ├── processor.ts
│ │ │ ├── redis.ts
│ │ │ ├── sequelize-config.ts
│ │ │ ├── sequelize-sync.ts
│ │ │ ├── sequelize.ts
│ │ │ ├── server-management.ts
│ │ │ ├── server.ts
│ │ │ ├── test/
│ │ │ │ ├── auth/
│ │ │ │ │ ├── providers/
│ │ │ │ │ │ └── google.spec.ts
│ │ │ │ │ ├── tokens.spec.ts
│ │ │ │ │ └── users.spec.ts
│ │ │ │ ├── domain/
│ │ │ │ │ ├── comments/
│ │ │ │ │ │ ├── fixture.ts
│ │ │ │ │ │ └── textSizes.spec.ts
│ │ │ │ │ └── topScores/
│ │ │ │ │ └── calculateTopScores.spec.ts
│ │ │ │ ├── fixture.ts
│ │ │ │ ├── integration/
│ │ │ │ │ ├── api/
│ │ │ │ │ │ ├── actions.spec.ts
│ │ │ │ │ │ ├── assignments.spec.ts
│ │ │ │ │ │ ├── assistant.spec.ts
│ │ │ │ │ │ ├── authorCounts.spec.ts
│ │ │ │ │ │ ├── editComment.spec.ts
│ │ │ │ │ │ ├── histogramScores.spec.ts
│ │ │ │ │ │ ├── simple-comment.spec.ts
│ │ │ │ │ │ ├── simple-ranges.spec.ts
│ │ │ │ │ │ ├── simple-user.spec.ts
│ │ │ │ │ │ └── test_helper.ts
│ │ │ │ │ └── websocket/
│ │ │ │ │ ├── assign_moderators.spec.ts
│ │ │ │ │ ├── update_notifications.spec.ts
│ │ │ │ │ └── websocket.spec.ts
│ │ │ │ ├── pipeline/
│ │ │ │ │ ├── pipeline.spec.ts
│ │ │ │ │ ├── rules.spec.ts
│ │ │ │ │ └── state.spec.ts
│ │ │ │ ├── test_helper.ts
│ │ │ │ └── unit/
│ │ │ │ ├── services/
│ │ │ │ │ ├── authorCounts.spec.ts
│ │ │ │ │ └── histogramScores.spec.ts
│ │ │ │ └── util/
│ │ │ │ └── notifications.spec.ts
│ │ │ └── worker.ts
│ │ └── tsconfig.json
│ └── frontend-web/
│ ├── .babelrc
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── css/
│ │ │ ├── fonts/
│ │ │ │ └── fonts.css
│ │ │ ├── moderator.css
│ │ │ └── normalize.css
│ │ └── index.html
│ ├── src/
│ │ ├── app/
│ │ │ ├── appstate.ts
│ │ │ ├── auth.ts
│ │ │ ├── components/
│ │ │ │ ├── Arrow/
│ │ │ │ │ ├── Arrow.tsx
│ │ │ │ │ ├── ArrowStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── AspectRatio/
│ │ │ │ │ ├── AspectRatio.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── AssignModerators/
│ │ │ │ │ ├── AssignModerators.tsx
│ │ │ │ │ └── AssignModeratorsStory.tsx
│ │ │ │ ├── AssignTagsForm.tsx
│ │ │ │ ├── Avatar/
│ │ │ │ │ ├── Avatar.tsx
│ │ │ │ │ ├── AvatarStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Button/
│ │ │ │ │ ├── Button.tsx
│ │ │ │ │ ├── ButtonStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── CanvasTruncate/
│ │ │ │ │ ├── CanvasTruncate.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── CheckboxRow/
│ │ │ │ │ ├── CheckboxRow.tsx
│ │ │ │ │ ├── CheckboxRowStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── CommentActionButton/
│ │ │ │ │ ├── CommentActionButton.tsx
│ │ │ │ │ ├── CommentActionButtonStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── CommentList/
│ │ │ │ │ ├── CommentList.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── CheckboxColumn/
│ │ │ │ │ │ │ ├── CheckboxColumn.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── SortColumn/
│ │ │ │ │ │ ├── SortColumn.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── CommentText.tsx
│ │ │ │ ├── ConfirmationCircle/
│ │ │ │ │ ├── ConfirmationCircle.tsx
│ │ │ │ │ ├── ConfirmationCircleStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── DotChart/
│ │ │ │ │ ├── DotChart.tsx
│ │ │ │ │ ├── DotChartStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ ├── comments-data.json
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ErrorRoot.tsx
│ │ │ │ ├── ErrorRootStory.tsx
│ │ │ │ ├── FlagsSummary.tsx
│ │ │ │ ├── HeaderBar.tsx
│ │ │ │ ├── Icons/
│ │ │ │ │ ├── IconBase.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── LazyLoadComment/
│ │ │ │ │ ├── CommentBodyStory.tsx
│ │ │ │ │ ├── LazyLoadComment.tsx
│ │ │ │ │ ├── components.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── MagicTimestamp.tsx
│ │ │ │ ├── MagicTimestampStory.tsx
│ │ │ │ ├── ModerateButtons/
│ │ │ │ │ ├── ModerateButtons.tsx
│ │ │ │ │ ├── ModerateButtonsStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── NavigationTab/
│ │ │ │ │ ├── NavigationTab.tsx
│ │ │ │ │ ├── NavigationTabStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── OverflowContainer/
│ │ │ │ │ ├── OverflowContainer.tsx
│ │ │ │ │ ├── OverflowContainerStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── RuleBars/
│ │ │ │ │ ├── RuleBars.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ScoresList/
│ │ │ │ │ ├── ScoresList.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Scrim/
│ │ │ │ │ ├── Scrim.tsx
│ │ │ │ │ ├── ScrimStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── SearchAttribute/
│ │ │ │ │ ├── SearchAttribute.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── SearchHeader/
│ │ │ │ │ ├── SearchHeader.tsx
│ │ │ │ │ ├── SearchHeaderStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── SingleComment/
│ │ │ │ │ ├── SingleComment.tsx
│ │ │ │ │ ├── SingleCommentStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── AnnotatedCommentText.tsx
│ │ │ │ │ │ ├── AuthorCounts.tsx
│ │ │ │ │ │ ├── CommentTags.tsx
│ │ │ │ │ │ ├── DetailRow.tsx
│ │ │ │ │ │ ├── FlagsList.tsx
│ │ │ │ │ │ └── SummaryScore.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Slider/
│ │ │ │ │ ├── RangeBar.tsx
│ │ │ │ │ ├── Slider.tsx
│ │ │ │ │ ├── SliderStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── SplashRoot.tsx
│ │ │ │ ├── SplashRootStory.tsx
│ │ │ │ ├── TagLabelRow/
│ │ │ │ │ ├── TagLabelRow.tsx
│ │ │ │ │ ├── TagLabelRowStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── ThemeRoot.tsx
│ │ │ │ ├── Toast/
│ │ │ │ │ ├── Toast.tsx
│ │ │ │ │ ├── ToastMessage.tsx
│ │ │ │ │ ├── ToastStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Toggle/
│ │ │ │ │ └── __spec__/
│ │ │ │ │ └── .gitkeep
│ │ │ │ ├── ToolTip/
│ │ │ │ │ ├── ToolTip.tsx
│ │ │ │ │ ├── ToolTipStory.tsx
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ └── index.ts
│ │ │ │ ├── VirtualListScrollbar.tsx
│ │ │ │ ├── article_controls.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── styles.ts
│ │ │ ├── config.ts
│ │ │ ├── injectors/
│ │ │ │ ├── articleFetchQueue.ts
│ │ │ │ ├── articleInjector.ts
│ │ │ │ ├── commentFetchQueue.ts
│ │ │ │ ├── commentInjector.ts
│ │ │ │ └── contextInjector.ts
│ │ │ ├── main.tsx
│ │ │ ├── platform/
│ │ │ │ ├── dataService.ts
│ │ │ │ ├── localStore.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── websocketService.ts
│ │ │ ├── scenes/
│ │ │ │ ├── Comments/
│ │ │ │ │ ├── Comments.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── CommentDetail/
│ │ │ │ │ │ │ ├── CommentDetail.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ └── InfoButton/
│ │ │ │ │ │ │ │ ├── InfoButton.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── store.ts
│ │ │ │ │ │ ├── ModeratedComments/
│ │ │ │ │ │ │ ├── ModeratedComments.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── store/
│ │ │ │ │ │ │ ├── checkedSelection.ts
│ │ │ │ │ │ │ ├── commentListLoader.ts
│ │ │ │ │ │ │ ├── currentPagingIdentifier.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── moderatedComments.ts
│ │ │ │ │ │ ├── NewComments/
│ │ │ │ │ │ │ ├── NewComments.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ └── BatchSelector/
│ │ │ │ │ │ │ │ ├── BatchSelector.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── store/
│ │ │ │ │ │ │ ├── checkedSelection.ts
│ │ │ │ │ │ │ ├── commentListLoader.ts
│ │ │ │ │ │ │ ├── commentScores.ts
│ │ │ │ │ │ │ ├── currentPagingIdentifier.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── util.ts
│ │ │ │ │ │ ├── Shortcuts/
│ │ │ │ │ │ │ ├── Shortcuts.tsx
│ │ │ │ │ │ │ ├── ShortcutsStory.tsx
│ │ │ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── SubheaderBar.tsx
│ │ │ │ │ │ ├── TagSelector/
│ │ │ │ │ │ │ ├── TagSelector.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── ThreadedCommentDetail/
│ │ │ │ │ │ ├── ThreadedCommentDetail.tsx
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ └── ThreadedComment/
│ │ │ │ │ │ │ ├── ThreadedComment.tsx
│ │ │ │ │ │ │ ├── ThreadedCommentStory.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── scoreFilters.ts
│ │ │ │ │ └── store.ts
│ │ │ │ ├── Login/
│ │ │ │ │ ├── ConfigureOAuth.tsx
│ │ │ │ │ ├── Login.tsx
│ │ │ │ │ ├── LoginStory.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── Search/
│ │ │ │ │ ├── Search.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── SearchResults.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── store/
│ │ │ │ │ │ ├── checkedSelection.ts
│ │ │ │ │ │ ├── commentListLoader.ts
│ │ │ │ │ │ ├── currentPagingIdentifier.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── searchResults.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── Settings/
│ │ │ │ │ ├── Ranges.tsx
│ │ │ │ │ ├── Settings.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── AddUsers.tsx
│ │ │ │ │ │ ├── ColorSelect/
│ │ │ │ │ │ │ ├── ColorSelect.tsx
│ │ │ │ │ │ │ ├── ColorSelectStory.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── EditUsers.tsx
│ │ │ │ │ │ ├── EditYouTubeUser.tsx
│ │ │ │ │ │ ├── LabelSettings/
│ │ │ │ │ │ │ ├── LabelSettings.tsx
│ │ │ │ │ │ │ ├── LabelSettingsStory.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── ManageAutomatedRules.tsx
│ │ │ │ │ │ ├── ManagePreselects.tsx
│ │ │ │ │ │ ├── ManageSensitivities.tsx
│ │ │ │ │ │ ├── ManageTags.tsx
│ │ │ │ │ │ ├── OAuthConfig.tsx
│ │ │ │ │ │ ├── RuleRow/
│ │ │ │ │ │ │ ├── RuleRow.tsx
│ │ │ │ │ │ │ ├── RuleRowStory.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── SaveButtons.tsx
│ │ │ │ │ │ ├── UserForm.tsx
│ │ │ │ │ │ ├── rows.tsx
│ │ │ │ │ │ └── users.tsx
│ │ │ │ │ ├── settingsStyles.ts
│ │ │ │ │ ├── store.ts
│ │ │ │ │ └── styles.ts
│ │ │ │ ├── Tables/
│ │ │ │ │ ├── ArticleTable.tsx
│ │ │ │ │ ├── CategorySidebar.tsx
│ │ │ │ │ ├── ComponentsStory.tsx
│ │ │ │ │ ├── FilterSidebar.tsx
│ │ │ │ │ ├── TableFrame.tsx
│ │ │ │ │ ├── TableFrameStory.tsx
│ │ │ │ │ ├── components.tsx
│ │ │ │ │ ├── styles.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── appstate.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── routes.ts
│ │ │ │ └── store.ts
│ │ │ ├── store.ts
│ │ │ ├── stores/
│ │ │ │ ├── appstate.ts
│ │ │ │ ├── articles.ts
│ │ │ │ ├── categories.ts
│ │ │ │ ├── commentActions.ts
│ │ │ │ ├── comments.ts
│ │ │ │ ├── counts.ts
│ │ │ │ ├── globalActions.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── preselects.ts
│ │ │ │ ├── rules.ts
│ │ │ │ ├── taggingSensitivities.ts
│ │ │ │ ├── tags.ts
│ │ │ │ ├── textSizes.ts
│ │ │ │ └── users.ts
│ │ │ ├── styles/
│ │ │ │ ├── breakpoints.ts
│ │ │ │ ├── colors.ts
│ │ │ │ ├── forms.ts
│ │ │ │ ├── header.ts
│ │ │ │ ├── hoverstates.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── scrim.ts
│ │ │ │ ├── typography.ts
│ │ │ │ ├── util.ts
│ │ │ │ └── zindex.ts
│ │ │ ├── stylesx/
│ │ │ │ └── index.ts
│ │ │ ├── util/
│ │ │ │ ├── DotChartRenderer.ts
│ │ │ │ ├── color.ts
│ │ │ │ ├── csrf.ts
│ │ │ │ ├── groupByColumn.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── makeCheckedSelectionStore/
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ └── makeCheckedSelectionStore.spec.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── makeCheckedSelectionStore.ts
│ │ │ │ ├── makeCurrentPagingIdentifierReducer.ts
│ │ │ │ ├── measureText.ts
│ │ │ │ ├── partial/
│ │ │ │ │ ├── __spec__/
│ │ │ │ │ │ ├── memoize.spec.ts
│ │ │ │ │ │ └── partial.spec.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── returnSavedCommentRow.ts
│ │ │ │ ├── returnURL.ts
│ │ │ │ ├── savedSorts.ts
│ │ │ │ ├── sortByLabel.ts
│ │ │ │ ├── time.ts
│ │ │ │ └── timeout.ts
│ │ │ └── utilx/
│ │ │ ├── cssInJs.ts
│ │ │ ├── highlightText.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── index.ts
│ │ │ ├── keyCodes.tsx
│ │ │ └── sortDefinitions.tsx
│ │ ├── index.ts
│ │ ├── models/
│ │ │ ├── article.ts
│ │ │ ├── category.ts
│ │ │ ├── comment.ts
│ │ │ ├── commentFlag.ts
│ │ │ ├── commentScore.ts
│ │ │ ├── common.ts
│ │ │ ├── fake/
│ │ │ │ ├── article.ts
│ │ │ │ ├── category.ts
│ │ │ │ ├── comment.ts
│ │ │ │ ├── commentScore.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── rule.ts
│ │ │ │ ├── tag.ts
│ │ │ │ └── user.ts
│ │ │ ├── index.ts
│ │ │ ├── preselect.ts
│ │ │ ├── rule.ts
│ │ │ ├── tag.ts
│ │ │ ├── taggingSensitivity.ts
│ │ │ └── user.ts
│ │ ├── server.ts
│ │ ├── test/
│ │ │ ├── actions.ts
│ │ │ ├── apitest.ts
│ │ │ ├── notificationChecks.ts
│ │ │ ├── objectChecks.ts
│ │ │ └── pageTests.ts
│ │ └── types.ts
│ ├── tooling/
│ │ ├── storybook/
│ │ │ ├── Storyshots.test.js
│ │ │ ├── __snapshots__/
│ │ │ │ └── Storyshots.test.js.snap
│ │ │ ├── config.js
│ │ │ ├── disable-aphrodite-inject.js
│ │ │ ├── jest.config.json
│ │ │ ├── preview-head.html
│ │ │ ├── register-context.js
│ │ │ └── webpack.config.js
│ │ ├── webpack.config.js
│ │ └── webpack.config.production.js
│ └── tsconfig.json
├── seed/
│ └── initial-database.sql
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2
jobs:
build:
docker:
- image: circleci/node:12.19
environment:
NODE_ENV: circle_ci
steps:
- checkout
- run: ./bin/install-circleci
- run: ./bin/lint
- run: ./bin/test
================================================
FILE: .dockerignore
================================================
.data
dist
**/dist
**/dist-commonjs
node_modules
**/node_modules
.vagrant
.dockerignore
Dockerfile
npm-debug.*
.git
.hg
.svn
config/local.json
================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
## Context
## Expected Behavior
## Actual Behavior
## Possible Fix
## Steps to Reproduce
1.
2.
3.
4.
## Context
## Your Environment
* Environment name and version (e.g. Chrome 39, etc):
* Operating System and version (desktop or mobile):
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
## Motivation and Context
## How Has This Been Tested?
## Screenshots (if appropriate):
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist:
- [ ] I have added tests to cover my changes.
================================================
FILE: .gitignore
================================================
.idea/
node_modules/
npm-debug.*
tmp/
.tmp/
dist/
dist-commonjs/
.sw[a-z]
.DS_Store
build/
*.js.map
*.css.map
.awcache
ansible_playbook.retry
.data
lerna-debug.log
source-context.json
source-contexts.json
*.rdb
================================================
FILE: CHANGELOG.md
================================================
# v1.0.5
* Fix bug where settings page ids are set to integers ([PR](https://github.com/conversationai/conversationai-moderator/pull/7))
* Update frontend validation for articleId to allow for alphanumeric ids ([PR](https://github.com/conversationai/conversationai-moderator/pull/6))
# v1.0.4
* Update data validation for publisher commentActions endpoint ([PR](https://github.com/conversationai/conversationai-moderator/pull/1))
# v1.0.0
* Open Source
================================================
FILE: CONTRIBUTING.md
================================================
# How to contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult [GitHub Help] for more
information on using pull requests.
[GitHub Help]: https://help.github.com/articles/about-pull-requests/
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright {2016} {Jigsaw}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: QUICKSTART.md
================================================
These are instructions on how to get a youtube instance of moderator running on a Google Cloud compute VM.
Step 0:
-------
Create a Google cloud project and a VM running 'Ubuntu 18.04 LTS Minimal' in the [Google console](https://console.cloud.google.com/compute/instances).
You'll also need to create a [firewall rule](https://console.cloud.google.com/networking/firewalls/list) to allow
HTTP traffic, and add that rule to your VM network tags.
You'll also need to allocate a domain name for your new VM - unfortunately the Google OAuth servers won't work with IP addresses.
Step 1:
-------
Create an OAuth2.0 Client ID entry in the [Google console](https://console.developers.google.com/apis/credentials).
Add the following Authorised redirect URIs:
```
http:///api/auth/callback/google
http:///api/youtube/callback
```
You'll be asked to enter this information when you first log into OS Moderator.
Step 2:
-------
Open a terminal to the VM and install the following packages.
```
sudo apt update
sudo apt dist-upgrade -y
sudo apt install -y nodejs npm docker.io git
sudo usermod -a -G docker `whoami`
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
exit
```
Step 3:
-------
Open a *new* terminal and download the code:
```
git clone git@github.com:conversationai/conversationai-moderator.git conversationai-moderator
cd conversationai-moderator
```
Step 4:
-------
Get a google cloud API key for the ConversationAI service. (To be documented...)
Step 5:
-------
Set the following environment variables
```
export MODERATOR_URL=http://
export GOOGLE_CLOUD_API_KEY=
export DATABASE_PASSWORD=password
Step 6:
-------
Run the service
```
docker-compose -f deployments/local/docker-compose.yml up -d
```
When it is up and running, point your browser in the right direction:
http:///
================================================
FILE: README.md
================================================
OSMod - The ConversationAI Moderator App
========================================
Deploying an OSMod instance
---------------------------
### Configuration
The configuration is found in packages/config/index.js. It is pretty self explanatory.
All settings can be overridden via environment variables.
Of particular note, the following have no sensible defaults, and
must be set in the environment before anything will work.
* `DATABASE_NAME`: The MySQL database name, e.g., 'os_moderator'.
* `DATABASE_USER`: The MySQL database user, e.g., 'os_moderator'.
* `DATABASE_PASSWORD`: The MySQL database password.
In a production setting, you'll also have to set the following:
* `MODERATOR_URL`: URL (including protocol, host and port) that OSMOD will listen on.
### System setup:
Install mysql, node (v10 or better), npm (v6 or better) and redis. Instructions for Ubuntu:
```bash
sudo apt install mysql-server nodejs npm redis
sudo apt install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
sudo npm install -g npm n
sudo n v10
hash -r
```
#### System setup -- Docker
If you want to run your moderator instances in one of the preconfigured docker containers,
you'll need to install docker. E.g., to install on Ubuntu 18.04 using apt
```bash
sudo apt install docker.io
# Add docker group to your account so you can talk to the local docker server.
# You probably need to log out and back in for groups to take effect.
sudo usermod -a -G docker `whoami`
sudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# check things work
docker version
docker-compose --version.
```
### Install dependencies and run the server
Install all node dependencies and run initial typescript compile.
```bash
./bin/install
```
Setup local MySQL:
```bash
sudo mysql << EOF
CREATE DATABASE $DATABASE_NAME;
CREATE USER IF NOT EXISTS '$DATABASE_USER' IDENTIFIED BY '$DATABASE_PASSWORD';
GRANT ALL on $DATABASE_NAME.* to $DATABASE_USER;
EOF
sudo mysql $DATABASE_NAME < seed/initial-database.sql
cd packages/backend-api
npx sequelize db:migrate
cd -
# Add a service user that can talk to the Perspective API:
bin/osmod users:create --group moderator --name "PerspectiveAPI" \
--moderator-type "perspective-api" --api-key $YOUR_PERSPECTIVE_API_KEY
# Run the server
bin/watch
```
#### Alternatively, run in a docker container
To run the service in a local docker container, run the following commands:
```bash
# Make sure any local instances of MySQL and Redis are not running
# E.g., on Ubuntu, stop the services
sudo systemctl stop mysql.service redis_6379.service redis-server.service
# Create docker images and launch the service
docker-compose -f deployments/local/docker-compose.yml up -d
```
The docker-compose scripts will initialise the database and create an API service user,
so you don't need to do those steps manually.
To shut down the service and delete all your containers:
```bash
docker-compose -f deployments/local/docker-compose.yml down
```
And to see what the container is doing:
```bash
docker-compose -f deployments/local/docker-compose.yml logs
```
The `osmod` CLI
---------------
You can manage your OSMod system using the osmod commandline tool:
```bash
./bin/osmod
```
where `command` is one of
* `users:create` Create new OS Moderator users
* `users:get-token` Get a JWT token for a user specified by id or email
* `comments:rescore` Rescore comment.
* `comments:send-to-scorer` Send comments to Endpoint of user object to get scored.
* `comments:calculate-text-size` Using node-canvas, calculate a single comment height at a given width.
* `comments:recalculate-text-sizes` Using node-canvas, recalculate comment heights at a given width.
* `comments:recalculate-top-scores` Recalculate comment top scores.
* `comments:flag` Flag comments.
* `comments:delete` Delete all comments from the database.
* `denormalize` Re-run denormalize counts
#### Managing Users
If you are an administrator, you can create other administrators, general moderator users,
and service users via the settings pages in the OSMod UI. Also, if there are no admin users,
the UI will turn the first user to log in into an admin. But you can also create users via the commandline.
Create a human user:
```bash
./bin/osmod users:create --group general --name "Name" --email "$EMAIL_OF_USER"
```
Replace `general` with `admin` if you want to create an administrator.
To create a service user - i.e., one that can connect via the API but not via the UI:
```bash
./bin/osmod users:create --group service --name "Robot"
```
Service users will require a JWT token. You can get this via the UI, or via running the following command:
```bash
./bin/osmod users:get-token --id 4
```
### Management commands
To run a local server on `:8080` and front-end on `:8000`
```bash
./bin/watch
```
### Lint
```bash
./bin/lint
```
optionally you can run lint-fix to attempt auto-fixing most lint errors
```bash
./bin/lint-fix
```
### Storybook
To preview individual widgets and components used by the the OSMod UI:
```bash
./bin/storybook
```
The frontend unit tests also use storybook to generate a HTML snapshot of the resulting widgets.
It then compares this snapshot to a stored version, allowing you to review and approve any changes.
To update stories that need new snapshots, go to `packages/frontend-web` and run
```bash
npm run storybook:test -- -u
```
## Development
The project uses [lerna](https://www.npmjs.com/package/lerna) to help manage
development [the several npm packages](packages/README.md) that are in this
repository. Lerna sym-links package dependencies within this repository. Lerna
is also used to publish updates to all these packages at once.
## Running tests
To run the tests, you'll need to tweak your enviornment:
```bash
# Some tests need admin privileges to clean out the database
export DATABASE_NAME=os_moderator_test
export DATABASE_USER=root
# Run all the tests
NODE_ENV=test bin/test
# or you can run individual tests:
cd packages/backend-api
NODE_ENV=test npm run test
NODE_ENV=test ../../node_modules/.bin/ts-mocha 'src/test/domain/comments/*.spec.js' --recursive --timeout 10000
```
The `bin/test` script uses lerna to first compile all the typescript to javascript,
then runs all the tests.
Deleting and recreating the database schema can take a very long time, hence the long timeout above.
You may need to increase this even further if your system is particularly slow.
If you want to run a test in the debugger, add the --inspect-brk flag to the mocha invocation,
then connect using the chrome inspector (URL: `chrome://inspect`).
## What a running service looks like
While there can be many ways to setup a service, in general a deployment will
typically be a single VM instance running these services:
A MySQL database that holds all of the applications state (See
[the data model doc](docs/modeling.md)).
* Frontend-Webserver service hosting the static ReactJS site. This sends
messages to the Backend API service.
* Backend API service responsible for querying the SQL database and sending
data to the front-end service. This is also the endpoint that receives
requests from the commenting platform it is supporting moderation of; and
it sends requests back to the commenting platform with user actions (e.g. to
reject or approve comments).
* Backend Work Queue service responsible for managing concurrent queue of
asynchronous work. TODO(ldixon): add reddis stuff?
* Some number of assistant services responsible for automating tasks.
Typically this is just calling ML services like
[the Perspective API](https://perspectiveapi.com/)
================================================
FILE: bin/build
================================================
#!/bin/bash
npx lerna run build
================================================
FILE: bin/initdb
================================================
#!/bin/bash
basename=`dirname $0`
mysqlx="mysql -u root -p${DATABASE_PASSWORD}"
if [ ! -z "${DATABASE_HOST}" ]; then
mysqlx="$mysqlx -h ${DATABASE_HOST}"
fi
until $mysqlx -e "" ; do
echo "Can't configure the database:-( waiting..."
sleep 10
done
if ! $mysqlx ${DATABASE_NAME} -e "select count(*) from SequelizeMeta;"; then
echo "Creating database and API service user."
echo
$mysqlx << EOF
CREATE DATABASE ${DATABASE_NAME};
CREATE USER '${DATABASE_USER}' IDENTIFIED BY '${DATABASE_PASSWORD}';
GRANT ALL on ${DATABASE_NAME}.* to ${DATABASE_USER};
EOF
$mysqlx ${DATABASE_NAME} < ${basename}/../seed/initial-database.sql
fi
echo "Running migrations."
cd ${basename}/../packages/backend-api
npx sequelize db:migrate
cd -
================================================
FILE: bin/install
================================================
#!/bin/bash
set -e
# Running npm install blats the package-lock.json file, as it doesn't know anything about
# the sub-packages.
# So if we are not doing a clean build, we save it off and restore it after installing tools.
basename=`dirname $0`
cd "${basename}/.."
if [ -f package-lock.json.bak ]; then
rm package-lock.json.bak
fi
if [ "$1" == "clean" ]; then
echo Removing old packages so we fetch everything from scratch
for i in . packages/backend-api packages/frontend-web; do
rm -Rf "${i}/node_modules" "${i}/package-lock.json"
done
else
cp package-lock.json package-lock.json.bak
fi
# Get packages for the root of the system, in particular lerna
npm install
if [ -f package-lock.json.bak ]; then
mv package-lock.json.bak package-lock.json
fi
# Use lerna to link together sub-modules, then build
./bin/link-packages
./bin/build
================================================
FILE: bin/install-circleci
================================================
#!/bin/bash
NODE_ENV=development npm install
./node_modules/.bin/lerna bootstrap
./node_modules/.bin/lerna run build
================================================
FILE: bin/link-packages
================================================
#!/bin/bash
npx lerna bootstrap
================================================
FILE: bin/lint
================================================
#!/bin/bash
./node_modules/.bin/lerna run lint
================================================
FILE: bin/lint-fix
================================================
#!/bin/bash
./node_modules/.bin/lerna run lint:fix
================================================
FILE: bin/osmod
================================================
#!/bin/bash
basename=`dirname $0`
# Forward to ${basename}/../packages/backend-api/bin/osmod.js
C=''
for i in "$@"; do
case "$i" in
*\'*)
i=`printf "%s" "$i" | sed "s/'/'\"'\"'/g"`
;;
*) : ;;
esac
C="$C '$i'"
done
bash -c "${basename}/../packages/backend-api/bin/osmod.js$C"
================================================
FILE: bin/run
================================================
#!/bin/bash
# Script to run some moderator component inside a docker container
set -e
basename=`dirname $0`
server=$1
logs=$2
if [ -z "${server}" ]; then
echo You need to specify a server name.
exit 1
fi
if [ -z "${logs}" ]; then
logs=/tmp/logs
echo "Using default log directory ($logs)"
else
echo Logging to $logs
fi
mkdir -p ${logs}
logs=`readlink -f ${logs}`
export FRONTEND_URL=${server}
export API_URL=${server}/api
# TODO fix initdb ${basename}/initdb
cd ${basename}/../packages/backend-api
now=`date`
echo Starting: $now >> ${logs}/server.log
echo Starting: $now >> ${logs}/processor.log
echo Starting: $now >> ${logs}/worker.log
node dist/processor.js 2>&1 | tee -a ${logs}/processor.log &
node dist/worker.js 2>&1 | tee -a ${logs}/worker.log &
node dist/server.js 2>&1 | tee -a ${logs}/server.log
================================================
FILE: bin/storybook
================================================
#!/bin/bash
cd packages/frontend-web
npm run storybook &
cd -
wait
================================================
FILE: bin/sync-db
================================================
#!/bin/bash
# keyword arguments
# 1st argument is .yaml file to pull env_variables from
# This script clears the terminal, attempts to pull db dump from dev,
# and apply it to your local db, overwriting it.
parse_yaml() {
local prefix=$2
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 |
awk -F$fs '{
indent = length($1)/2;
vname[indent] = $2;
for (i in vname) {if (i > indent) {delete vname[i]}}
if (length($3) > 0) {
vn=""; for (i=0; i os_moderator.sql
echo "Finished downloading"
echo
echo "restoring db from .sql backup"
echo
mysql -uroot os_moderator -e "SOURCE os_moderator.sql"
rm os_moderator.sql
echo "mysqldump is complete"
================================================
FILE: bin/test
================================================
#!/bin/bash
npx lerna run compile
npx lerna run test
================================================
FILE: bin/watch
================================================
#!/bin/bash
set -e
export FRONTEND_URL=http://localhost:8000
export API_URL=http://localhost:8080
if [ -z "$1" ]; then
FRONTEND=1
BACKEND=1
PROCESSING=1
else
while [ -n "$1" ]; do
if [ "$1" == frontend ]; then
FRONTEND=1
elif [ "$1" == backend ]; then
BACKEND=1
elif [ "$1" == processing ]; then
BACKEND=1
PROCESSING=1
fi
shift
done
fi
if [ -n "$FRONTEND" ]; then
cd packages/frontend-web
npm run watch &
cd -
fi
if [ -n "$BACKEND" ]; then
cd packages/frontend-web
npm run compile:lib
cd -
cd packages/backend-api
npx ts-node-dev --inspect=5858 src/server.ts &
if [ -n "$PROCESSING" ]; then
npx ts-node-dev --inspect=5857 src/processor.ts &
npx ts-node-dev --inspect=5856 src/worker.ts &
fi
cd -
fi
wait
================================================
FILE: deployments/gcloud/Dockerfile
================================================
FROM gcr.io/google_appengine/nodejs
RUN install_node v8.11.1
WORKDIR /app/
COPY . /app/
RUN npm cache verify
RUN bin/install
EXPOSE 8000 8080
CMD bin/run
================================================
FILE: deployments/gcloud/README.md
================================================
# Deploying ConversationAI Moderator to Google Cloud
## Preparing for the deployment
### Install `gcloud`, `docker`, `kubectl` etc.
Instructions for installing gcloud can be found [here](https://cloud.google.com/sdk/docs/quickstart-linux).
You'll find instructions for installing docker in the [root README](../../README.md)
Once you've installed gcloud and docker, run the following commands to prepare
the system for installing moderator:
```bash
gcloud components install kubectl alpha
gcloud auth configure-docker
```
### Create a GCloud project
Before you can do anything else, you need to create a Google Cloud project,
and assign a billing account
There are many instructions on how to do this via the console. If you want
to do it via the commandline, run the following commands:
```bash
# You can see a list of your billing IDs by running
gcloud alpha billing accounts list
# Set up the project details
PROJECT=
REGION=
BILLING=
gcloud projects create $PROJECT --name="Conversation AI Modereator"
gcloud alpha billing projects link $PROJECT --billing-account=$BILLING
gcloud config set project $PROJECT
gcloud config set compute/zone $REGION
# Probably not an exaustive list. Update if you discover any that are
# missing
gcloud services enable sql-component.googleapis.com sqladmin.googleapis.com
```
### Allocate a domain name and IP address
You will probably want to allocate a domain name and static IP address
for your moderator instance, especially for the production case. You can
find instructions on how to do this [here](https://cloud.google.com/kubernetes-engine/docs/tutorials/configuring-domain-name-static-ip).
(TODO: not yet integrated with the scripts. Need to add static IP address as an
environment item, and use it to set up Google OAuth.)
Once you've allocated a hostname and IP address, you'll have enough information
to set the API_URL and FRONTEND_URL environment variables, and to configure
the
### Set up the Google Cloud SQL proxy
During the deployment process, we need to connect to the Google Cloud MySQL
instance to initialise and populate the database. Also, we'll need access
to provision to proopulate users via the CLI.
To do this, we'll need to create a connection using the cloud SQL proxy.
To create the /cloudsql directory and fetch the cloud_sql_proxy script, run
the following:
```bash
sudo mkdir -p /cloudsql
sudo chmod 777 /cloudsql
wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O /cloudsql/cloud_sql_proxy
chmod +x /cloudsql/cloud_sql_proxy
```
You'll also need to create a service user with the necessary permissions,
then create a key file with that user's private key:
```bash
SQL_MANAGER=sql-manager
gcloud iam service-accounts create $SQL_MANAGER --display-name "SQL Manager"
gcloud projects add-iam-policy-binding $PROJECT \
--member serviceAccount:$SQL_MANAGER@$PROJECT.iam.gserviceaccount.com \
--role roles/cloudsql.client
gcloud iam service-accounts keys create /cloudsql/key.json \
--iam-account $SQL_MANAGER@$PROJECT.iam.gserviceaccount.com
```
If you want to connect from a different machine, skip this second step. Instead, once
you have set up the `/cloudsql` directory as described above, just copy the
`/cloudsql/key.json` file into place and you are good to go.
To start the SQL proxy and connect to the database identified by `$SQL_INSTANCE_NAME`,
run the following commands:
```bash
SQL_CONNECTION=`gcloud sql instances describe $SQL_INSTANCE_NAME --format "value(connectionName)"`
/cloudsql/cloud_sql_proxy -dir=/cloudsql -instances=$SQL_CONNECTION -credential_file=/cloudsql/key.json &
```
You'll then be able to access the database via the appropriate socket file in `/cloudsql`, e.g., :
```bash
DATABASE_SOCKET=/cloudsql/$SQL_CONNECTION bin/osmod users:create --group general --name "Name" --email "email@example.com"
mysql --socket=/cloudsql/$SQL_CONNECTION --user=root --password=$DATABASE_PASSWORD
```
## Do the deployment
### Generate a docker file and upload it to a registry
You can skip this step if you've got a prerolled docker image for the moderator.
(You'll need to do this step if you are rolling out a new version.
```bash
MODERATOR_IMAGE_ID=eu.gcr.io/$PROJECT/conversationai-moderator:
cd
docker build -f deployments/gcloud/Dockerfile -t $MODERATOR_IMAGE_ID .
docker push $MODERATOR_IMAGE_ID
```
You can test out your docker image by running:
```bash
docker run --publish 8080:8080 --publish 8000:8000 \
--env DATABASE_SOCKET=/cloudsql/$SQL_CONNECTION \
--env DATABASE_NAME=$DATABASE_NAME \
--env DATABASE_USER=$DATABASE_USER \
--env DATABASE_PASSWORD=$DATABASE_PASSWORD \
--env GOOGLE_SCORE_AUTH=$GOOGLE_SCORE_AUTH \
--mount type=bind,source=/cloudsql,destination=/cloudsql/
$MODERATOR_IMAGE_ID
```
You can adjust the above environment settings to connect to the database instance
you require. The above settings assume you are connecting to the
### Set up an `ENVIRONMENT` file and run the deploy script
Subsequent steps need you to set a large number of parameters. The easiest way
to do this is to create an environment file. E.g.,
```
cat > ENVIRONMENT << EOF
export PROJECT=
export REGION=
export BILLING=
export DATABASE_NAME=os_moderator
export DATABASE_USER=os_moderator
export DATABASE_PASSWORD=password
export GOOGLE_SCORE_AUTH=
export FRONTEND_URL=http:///
export API_URL=http://:8080/
export MODERATOR_IMAGE_ID=eu.gcr.io/$PROJECT/conversationai-moderator:
export SQL_INSTANCE_NAME=conversationai-moderator-db
export SQL_MANAGER=sql-manager
```
### Deploy the MySQL database
Install and configure the MySQL database.
```bash
. ENVIRONMENT
./deploy-sql.sh
```
You'll only need to do this once.
### Deploy the app using Kubernetes
First of all, create your kubernetes cluster. For normal usage, you only need to
create a cluster with one node. We assume the cluster is called
`conversationai-moderator`.
```bash
. ENVIRONMENT
gcloud container clusters create conversationai-moderator --num-nodes=1 --region=$REGION
```
Next, deploy the moderator app. You'll need to rerun this step every time
you want to upgrade the moderator.
```bash
. ENVIRONMENT
./deploy.sh
```
You can see the state of the app in the Kubernetes console, or by running
```bash
kubectl describe deployments conversationai-moderator
```
## TODO:
- Integrate statically allocated IP address
e.g., https://cloud.google.com/kubernetes-engine/docs/tutorials/configuring-domain-name-static-ip
- Separate frontend and api into separate containers?
- Enable SSH in the load balancer
================================================
FILE: deployments/gcloud/deploy-sql.sh
================================================
#!/bin/bash
# Create a managed SQL instance and populate it with initial data
# This script assumes the following environment variables have been set
# SQL_INSTANCE_NAME - Label to use for the ConversationAI MySQL instance
# DATABASE_NAME - Name of the database
# DATABASE_USER - Database user
# DATABASE_PASSWORD - Database password
# It also assumes that gcloud is configured to manage the correct Google
# Cloud project and compute region, and that an appropriate service account
# has been created. See the README for details on how to set these things
# up
set -e
set -u
if [ -z "$SQL_INSTANCE_NAME" ]; then
echo "SQL_INSTANCE_NAME is not defined"
exit;
fi
if [ -z "$DATABASE_NAME" ]; then
echo "DATABASE_NAME is not defined"
exit;
fi
if [ -z "$DATABASE_USER" ]; then
echo "DATABASE_USER is not defined"
exit;
fi
if [ -z "$DATABASE_PASSWORD" ]; then
echo "DATABASE_PASSWORD is not defined"
exit;
fi
gcloud sql instances create $SQL_INSTANCE_NAME --tier=db-g1-small --database-version=MYSQL_5_7
gcloud sql users set-password root % --instance $SQL_INSTANCE_NAME --password $DATABASE_PASSWORD
gcloud sql users create $DATABASE_USER % --instance=$SQL_INSTANCE_NAME --password=$DATABASE_PASSWORD
gcloud sql databases create $DATABASE_NAME --instance=$SQL_INSTANCE_NAME
export SQL_CONNECTION=`gcloud sql instances describe $SQL_INSTANCE_NAME --format "value(connectionName)"`
# Set up SQL proxy on local machine so we can tunnel through the firewall and access the database
/cloudsql/cloud_sql_proxy -dir=/cloudsql -instances=$SQL_CONNECTION -credential_file=/cloudsql/key.json &
mysql --socket=/cloudsql/$SQL_CONNECTION --user=root --password=$DATABASE_PASSWORD << EOF
GRANT ALL on $DATABASE_NAME.* to $DATABASE_USER
EOF
mysql --socket=/cloudsql/$SQL_CONNECTION --user=$DATABASE_USER --password=$DATABASE_PASSWORD $DATABASE_NAME < seed/initial-database.sql
================================================
FILE: deployments/gcloud/deploy.sh
================================================
#!/bin/bash
# Use kubectl to deploy the app to the
set -e
set -u
export SQL_CONNECTION=`gcloud sql instances describe $SQL_INSTANCE_NAME --format "value(connectionName)"`
# TODO: May need to destroy secrets first.
kubectl create secret generic cloudsql-instance-credentials --from-file=credentials.json=/cloudsql/key.json
kubectl create secret generic moderator-configuration \
--from-literal=DATABASE_NAME=$DATABASE_NAME \
--from-literal=DATABASE_USER=$DATABASE_USER \
--from-literal=DATABASE_PASSWORD=$DATABASE_PASSWORD \
--from-literal=GOOGLE_SCORE_AUTH=$GOOGLE_SCORE_AUTH \
--from-literal=SQL_CONNECTION=$SQL_CONNECTION
envsubst < kubernetes-deployment.yaml | kubectl apply -f -
# TODO: Use a static IP address if one is allocated.
kubectl apply -f kubernetes-networking.yaml
================================================
FILE: deployments/gcloud/kubernetes-deployment.yaml
================================================
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: conversationai-moderator
labels:
app: conversationai-moderator
spec:
template:
metadata:
labels:
app: conversationai-moderator
spec:
containers:
- name: moderator
# TODO Need to replace this with image created by deployment script
image: ${MODERATOR_IMAGE_ID}:latest
ports:
- containerPort: 8000
hostPort: 80
- containerPort: 8080
hostPort: 8080
env:
- name: DATABASE_NAME
valueFrom:
secretKeyRef:
name: moderator-configuration
key: DATABASE_NAME
- name: DATABASE_USER
valueFrom:
secretKeyRef:
name: moderator-configuration
key: DATABASE_USER
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: moderator-configuration
key: DATABASE_PASSWORD
- name: GOOGLE_SCORE_AUTH
valueFrom:
secretKeyRef:
name: moderator-configuration
key: GOOGLE_SCORE_AUTH
- name: cloudsql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.11
command: ["/cloud_sql_proxy"]
args: ["-instances=$(SQL_CONNECTION)=tcp:3306",
"-credential_file=/secrets/cloudsql/credentials.json"]
env:
- name: SQL_CONNECTION
valueFrom:
secretKeyRef:
name: moderator-configuration
key: SQL_CONNECTION
volumeMounts:
- name: cloudsql-instance-credentials
mountPath: /secrets/cloudsql
readOnly: true
- name: redis-server
image: launcher.gcr.io/google/redis3
volumes:
- name: cloudsql-instance-credentials
secret:
secretName: cloudsql-instance-credentials
================================================
FILE: deployments/gcloud/kubernetes-networking.yaml
================================================
apiVersion: v1
kind: Service
metadata:
name: moderator-networking
spec:
type: LoadBalancer
ports:
- name: frontend
port: 80
targetPort: 8000
protocol: TCP
- name: api
port: 8080
targetPort: 8080
protocol: TCP
selector:
app: conversationai-moderator
================================================
FILE: deployments/local/Dockerfile
================================================
FROM gcr.io/google_appengine/nodejs
RUN install_node v8.11.1 && apt update && apt dist-upgrade -y && apt install -y mysql-client
WORKDIR /app/
COPY . /app/
RUN npm cache verify && bin/install
EXPOSE 80 80
CMD bin/run
================================================
FILE: deployments/local/README.md
================================================
# Run application locally in a docker collection
The scripts and configuration files contained in this directory create 3 containers:
- A MySQL database container
- A Redis datastore container
- A container for everything else
The contents of the latter container are taken from the local filesystem.
You'll find details on how to create and use these containers in the
root [README.md](../../README.md)
================================================
FILE: deployments/local/docker-compose.yml
================================================
version: '3'
services:
database:
container_name: database
image: 'mysql:5.7.16'
volumes:
- './.data/db:/var/lib/mysql'
restart: always
environment:
MYSQL_ROOT_PASSWORD: "${DATABASE_PASSWORD}"
MYSQL_DATABASE: 'os_moderator'
MYSQL_USER: 'os_moderator'
MYSQL_PASSWORD: "${DATABASE_PASSWORD}"
ports:
- '3306:3306'
redis:
container_name: redis
image: 'redis:3.2.1'
ports:
- '6379:6379'
server:
build:
context: ../..
dockerfile: "deployments/local/Dockerfile"
environment:
DATABASE_HOST: database
DATABASE_NAME: 'os_moderator'
DATABASE_USER: 'os_moderator'
DATABASE_PASSWORD: "${DATABASE_PASSWORD}"
REDIS_URL: 'redis://redis:6379'
HTTPS_LINKS_ONLY: 'false'
APP_NAME: 'Moderator'
GOOGLE_CLOUD_API_KEY: "${GOOGLE_CLOUD_API_KEY}"
PORT: 80
ports:
- "80:80"
links:
- database
- redis
================================================
FILE: deployments/standalone/.dockerignore
================================================
.data
dist
dist-commonjs
node_modules
.vagrant
.dockerignore
Dockerfile
npm-debug.*
.git
.hg
.svn
config/local.json
================================================
FILE: deployments/standalone/Dockerfile
================================================
FROM ubuntu:bionic
RUN apt update && apt --assume-yes dist-upgrade && apt --assume-yes install mysql-server nodejs npm redis supervisor && npm install -g npm
ENV DATABASE_PASSWORD=$DATABASE_PASSWORD
WORKDIR /app
COPY . /app
RUN bin/install
RUN deployments/standalone/initialise_db.sh
================================================
FILE: deployments/standalone/initialise_db.sh
================================================
#!/bin/bash
mkdir -p /var/run/mysqld
chown mysql:mysql /var/run/mysqld/
/usr/bin/mysqld_safe --skip-grant-tables --pid-file=/run/mysqld/mysqld.pid &
sleep 5
mysql -u root << EOF
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
CREATE DATABASE os_moderator;
CREATE USER 'os_moderator' IDENTIFIED BY '$DATABASE_PASSWORD';
GRANT ALL on os_moderator.* to os_moderator;
EOF
mysql os_moderator < seed/initial-database.sql
mysql -u root << EOF
UPDATE mysql.user SET Password=PASSWORD('$DATABASE_PASSWORD') WHERE User='root';
EOF
cd ${basename}/../packages/backend-api
npx sequelize db:migrate
cd -
================================================
FILE: design-files/Moderator-StickerSheet-20161117-DAS/document.json
================================================
{"_class":"document","do_objectID":"FDD7F69B-2AF0-4FAE-9B51-45D7040E8FCB","assets":{"_class":"assetCollection","colors":[],"gradients":[],"imageCollection":{"_class":"imageCollection","images":{}},"images":[]},"currentPageIndex":0,"enableLayerInteraction":true,"enableSliceInteraction":true,"foreignSymbols":[],"layerStyles":{"_class":"sharedStyleContainer","objects":[{"_class":"sharedStyle","do_objectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","name":"Material\/Icon dark","value":{"_class":"style","contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":0.5399999618530273},"endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0}},{"_class":"sharedStyle","do_objectID":"E9914C62-FB70-43EA-B840-F1BE2DEEA401","name":"Material\/Light\/Menu","value":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0.9803921568627451,"green":0.9803921568627451,"red":0.9803921568627451},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"shadows":[{"_class":"shadow","isEnabled":true,"blurRadius":8,"color":{"_class":"color","alpha":0.24,"blue":0,"green":0,"red":0},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"offsetX":0,"offsetY":8,"spread":0},{"_class":"shadow","isEnabled":true,"blurRadius":8,"color":{"_class":"color","alpha":0.12,"blue":0,"green":0,"red":0},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"offsetX":0,"offsetY":0,"spread":0}],"sharedObjectID":"E9914C62-FB70-43EA-B840-F1BE2DEEA401","startDecorationType":0}},{"_class":"sharedStyle","do_objectID":"ACF14791-EE9C-41D8-9F0E-C0BD9795FFE9","name":"Material\/Light\/Dialog","value":{"_class":"style","borders":[{"_class":"border","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0.592,"green":0.592,"red":0.592},"fillType":1,"gradient":{"_class":"gradient","elipseLength":1,"from":"{0.5, 0}","gradientType":0,"shouldSmoothenOpacity":false,"stops":[{"_class":"gradientStop","color":{"_class":"color","alpha":0,"blue":0,"green":0,"red":0},"position":0},{"_class":"gradientStop","color":{"_class":"color","alpha":0,"blue":0,"green":0,"red":0},"position":0.95},{"_class":"gradientStop","color":{"_class":"color","alpha":0.04,"blue":0,"green":0,"red":0},"position":1}],"to":"{0.5, 1}"},"position":1,"thickness":0.5},{"_class":"border","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0.592,"green":0.592,"red":0.592},"contextSettings":{"_class":"graphicsContextSettings","blendMode":8,"opacity":1},"fillType":1,"gradient":{"_class":"gradient","elipseLength":1,"from":"{0.5, 0}","gradientType":0,"shouldSmoothenOpacity":false,"stops":[{"_class":"gradientStop","color":{"_class":"color","alpha":0.8,"blue":1,"green":1,"red":1},"position":0},{"_class":"gradientStop","color":{"_class":"color","alpha":0.4,"blue":1,"green":1,"red":1},"position":0.04936005799731481},{"_class":"gradientStop","color":{"_class":"color","alpha":0,"blue":1,"green":1,"red":1},"position":0.2},{"_class":"gradientStop","color":{"_class":"color","alpha":0,"blue":1,"green":1,"red":1},"position":1}],"to":"{0.5, 1}"},"position":1,"thickness":0.5}],"endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"shadows":[{"_class":"shadow","isEnabled":true,"blurRadius":24,"color":{"_class":"color","alpha":0.3,"blue":0,"green":0,"red":0},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"offsetX":0,"offsetY":24,"spread":0},{"_class":"shadow","isEnabled":true,"blurRadius":24,"color":{"_class":"color","alpha":0.22,"blue":0,"green":0,"red":0},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"offsetX":0,"offsetY":0,"spread":0}],"sharedObjectID":"ACF14791-EE9C-41D8-9F0E-C0BD9795FFE9","startDecorationType":0}}]},"layerSymbols":{"_class":"symbolContainer","objects":[]},"layerTextStyles":{"_class":"sharedTextStyleContainer","objects":[{"_class":"sharedStyle","do_objectID":"CB2CC42C-F552-418E-A76F-6D0EDA1AD3BF","name":"Section","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"CB2CC42C-F552-418E-A76F-6D0EDA1AD3BF","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNALAAAAAAAAF8QGUlUQ0ZyYW5rbGluR290aGljU3RkLURlbWnSHB0eH1okY2xhc3NuYW1lWCRjbGFzc2VzXxATTlNNdXRhYmxlRGljdGlvbmFyeaMeICFcTlNEaWN0aW9uYXJ5WE5TT2JqZWN00hwdIyRfEBBOU0ZvbnREZXNjcmlwdG9yoiUhXxAQTlNGb250RGVzY3JpcHRvcl8QD05TS2V5ZWRBcmNoaXZlctEoKVRyb290gAEACAARABoAIwAtADIANwBBAEcATABTAHAAcgB0AHsAgwCOAJEAkwCVAJgAmgCcAJ4AtADKANMA7wD0AP8BCAEeASIBLwE4AT0BUAFTAWYBeAF7AYAAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAABgg=="},"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NPECcwLjE0OTAxOTYwNzggMC4xNDkwMTk2MDc4IDAuMTQ5MDE5NjA3OAAQAYAC0hAREhNaJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0NvbG9yohIUWE5TT2JqZWN0XxAPTlNLZXllZEFyY2hpdmVy0RcYVHJvb3SAAQgRGiMtMjc7QUhOW2KMjpCVoKmxtL3P0tcAAAAAAAABAQAAAAAAAAAZAAAAAAAAAAAAAAAAAAAA2Q=="},"NSKern":0,"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGIyRYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBUZH1UkbnVsbNYJCgsMDQ4PEBEQExBfEBJOU1BhcmFncmFwaFNwYWNpbmdaTlNUYWJTdG9wc18QEk5TV3JpdGluZ0RpcmVjdGlvblxOU1RleHRCbG9ja3NWJGNsYXNzW05TVGV4dExpc3RzI0AkAAAAAAAAgAIQAYACgASAAtIWDRcYWk5TLm9iamVjdHOggAPSGhscHVokY2xhc3NuYW1lWCRjbGFzc2VzV05TQXJyYXmiHB5YTlNPYmplY3TSGhsgIV8QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxloyAiHl8QEE5TUGFyYWdyYXBoU3R5bGVfEA9OU0tleWVkQXJjaGl2ZXLRJSZUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAFAAZQBwAIUAkgCZAKUArgCwALIAtAC2ALgAvQDIAMkAywDQANsA5ADsAO8A+AD9ARcBGwEuAUABQwFIAAAAAAAAAgEAAAAAAAAAJwAAAAAAAAAAAAAAAAAAAUo="}}}}},{"_class":"sharedStyle","do_objectID":"D531A342-3DE7-4169-AF05-BE4F54B37453","name":"Article title","value":{"_class":"style","contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":0.86},"endDecorationType":0,"miterLimit":10,"sharedObjectID":"D531A342-3DE7-4169-AF05-BE4F54B37453","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAMAAAAAAAAF8QD0NoZWx0ZW5oYW0tQm9va9IcHR4fWiRjbGFzc25hbWVYJGNsYXNzZXNfEBNOU011dGFibGVEaWN0aW9uYXJ5ox4gIVxOU0RpY3Rpb25hcnlYTlNPYmplY3TSHB0jJF8QEE5TRm9udERlc2NyaXB0b3KiJSFfEBBOU0ZvbnREZXNjcmlwdG9yXxAPTlNLZXllZEFyY2hpdmVy0SgpVHJvb3SAAQAIABEAGgAjAC0AMgA3AEEARwBMAFMAcAByAHQAewCDAI4AkQCTAJUAmACaAJwAngC0AMoA0wDlAOoA9QD+ARQBGAElAS4BMwFGAUkBXAFuAXEBdgAAAAAAAAIBAAAAAAAAACoAAAAAAAAAAAAAAAAAAAF4"},"NSStrokeWidth":0,"NSKern":0,"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGHyBYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBEVHFUkbnVsbNQJCgsMDQ4PEFVOU1JHQlxOU0NvbG9yU3BhY2VfEBJOU0N1c3RvbUNvbG9yU3BhY2VWJGNsYXNzRjAgMCAwABABgAKABNISDBMUVE5TSUQQAYAD0hYXGBlaJGNsYXNzbmFtZVgkY2xhc3Nlc1xOU0NvbG9yU3BhY2WiGhtcTlNDb2xvclNwYWNlWE5TT2JqZWN00hYXHR5XTlNDb2xvcqIdG18QD05TS2V5ZWRBcmNoaXZlctEhIlRyb290gAEACAARABoAIwAtADIANwA9AEMATABSAF8AdAB7AIIAhACGAIgAjQCSAJQAlgCbAKYArwC8AL8AzADVANoA4gDlAPcA+gD\/AAAAAAAAAgEAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAQE="},"NSStrokeColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NMMC42IDAuNiAwLjYAEAKAAtIQERITWiRjbGFzc25hbWVYJGNsYXNzZXNXTlNDb2xvcqISFFhOU09iamVjdF8QD05TS2V5ZWRBcmNoaXZlctEXGFRyb290gAEIERojLTI3O0FITltib3FzeIOMlJegsrW6AAAAAAAAAQEAAAAAAAAAGQAAAAAAAAAAAAAAAAAAALw="},"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGJSZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBcbIVUkbnVsbNgJCgsMDQ4PEBESExQVFhYVViRjbGFzc1pOU1RhYlN0b3BzXU5TSGVhZGVyTGV2ZWxbTlNUZXh0TGlzdHNfEB9OU0FsbG93c1RpZ2h0ZW5pbmdGb3JUcnVuY2F0aW9uXxAPTlNNYXhMaW5lSGVpZ2h0XxAPTlNNaW5MaW5lSGVpZ2h0XxASTlNXcml0aW5nRGlyZWN0aW9ugASAACNACAAAAAAAAIACEAEjQDgAAAAAAADSGAkZGlpOUy5vYmplY3RzoIAD0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0FycmF5oh4gWE5TT2JqZWN00hwdIiNfEBdOU011dGFibGVQYXJhZ3JhcGhTdHlsZaMiJCBfEBBOU1BhcmFncmFwaFN0eWxlXxAPTlNLZXllZEFyY2hpdmVy0ScoVHJvb3SAAQAIABEAGgAjAC0AMgA3AD0AQwBUAFsAZgB0AIAAogC0AMYA2wDdAN8A6ADqAOwA9QD6AQUBBgEIAQ0BGAEhASkBLAE1AToBVAFYAWsBfQGAAYUAAAAAAAACAQAAAAAAAAApAAAAAAAAAAAAAAAAAAABhw=="}}}}},{"_class":"sharedStyle","do_objectID":"C25F20A1-60B3-40B1-BCDE-FC0E9111F18D","name":"Hello Lucas! Style","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"C25F20A1-60B3-40B1-BCDE-FC0E9111F18D","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNALAAAAAAAAF8QGUlUQ0ZyYW5rbGluR290aGljU3RkLURlbWnSHB0eH1okY2xhc3NuYW1lWCRjbGFzc2VzXxATTlNNdXRhYmxlRGljdGlvbmFyeaMeICFcTlNEaWN0aW9uYXJ5WE5TT2JqZWN00hwdIyRfEBBOU0ZvbnREZXNjcmlwdG9yoiUhXxAQTlNGb250RGVzY3JpcHRvcl8QD05TS2V5ZWRBcmNoaXZlctEoKVRyb290gAEACAARABoAIwAtADIANwBBAEcATABTAHAAcgB0AHsAgwCOAJEAkwCVAJgAmgCcAJ4AtADKANMA7wD0AP8BCAEeASIBLwE4AT0BUAFTAWYBeAF7AYAAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAABgg=="},"NSKern":0,"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGHyBYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBEVHFUkbnVsbNQJCgsMDQ4PEFVOU1JHQlxOU0NvbG9yU3BhY2VfEBJOU0N1c3RvbUNvbG9yU3BhY2VWJGNsYXNzRjEgMSAxABABgAKABNISDBMUVE5TSUQQAYAD0hYXGBlaJGNsYXNzbmFtZVgkY2xhc3Nlc1xOU0NvbG9yU3BhY2WiGhtcTlNDb2xvclNwYWNlWE5TT2JqZWN00hYXHR5XTlNDb2xvcqIdG18QD05TS2V5ZWRBcmNoaXZlctEhIlRyb290gAEACAARABoAIwAtADIANwA9AEMATABSAF8AdAB7AIIAhACGAIgAjQCSAJQAlgCbAKYArwC8AL8AzADVANoA4gDlAPcA+gD\/AAAAAAAAAgEAAAAAAAAAIwAAAAAAAAAAAAAAAAAAAQE="},"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGJSZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBcbIVUkbnVsbNcJCgsMDQ4PEBESExQVFlYkY2xhc3NaTlNUYWJTdG9wc11OU0hlYWRlckxldmVsXxASTlNQYXJhZ3JhcGhTcGFjaW5nXxAPTlNNaW5MaW5lSGVpZ2h0XxASTlNXcml0aW5nRGlyZWN0aW9uW05TQWxpZ25tZW50gASAAiM\/8AAAAAAAACNAJAAAAAAAACNAPIAAAAAAABABEATSGAkZGlpOUy5vYmplY3RzoIAD0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0FycmF5oh4gWE5TT2JqZWN00hwdIiNfEBdOU011dGFibGVQYXJhZ3JhcGhTdHlsZaMiJCBfEBBOU1BhcmFncmFwaFN0eWxlXxAPTlNLZXllZEFyY2hpdmVy0ScoVHJvb3SAAQAIABEAGgAjAC0AMgA3AD0AQwBSAFkAZAByAIcAmQCuALoAvAC+AMcA0ADZANsA3QDiAO0A7gDwAPUBAAEJAREBFAEdASIBPAFAAVMBZQFoAW0AAAAAAAACAQAAAAAAAAApAAAAAAAAAAAAAAAAAAABbw=="}}}}},{"_class":"sharedStyle","do_objectID":"63446B2F-1571-456D-89FD-2A26EA213F12","name":"Material\/Dark\/Title","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"63446B2F-1571-456D-89FD-2A26EA213F12","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNANAAAAAAAAF1Sb2JvdG8tTWVkaXVt0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc18QE05TTXV0YWJsZURpY3Rpb25hcnmjHiAhXE5TRGljdGlvbmFyeVhOU09iamVjdNIcHSMkXxAQTlNGb250RGVzY3JpcHRvcqIlIV8QEE5TRm9udERlc2NyaXB0b3JfEA9OU0tleWVkQXJjaGl2ZXLRKClUcm9vdIABAAgAEQAaACMALQAyADcAQQBHAEwAUwBwAHIAdAB7AIMAjgCRAJMAlQCYAJoAnACeALQAygDTAOEA5gDxAPoBEAEUASEBKgEvAUIBRQFYAWoBbQFyAAAAAAAAAgEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAAXQ="},"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NGMSAxIDEAEAGAAtIQERITWiRjbGFzc25hbWVYJGNsYXNzZXNXTlNDb2xvcqISFFhOU09iamVjdF8QD05TS2V5ZWRBcmNoaXZlctEXGFRyb290gAEIERojLTI3O0FITltiaWttcn2GjpGarK+0AAAAAAAAAQEAAAAAAAAAGQAAAAAAAAAAAAAAAAAAALY="},"NSLigature":0,"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGIiNYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBQYHlUkbnVsbNYJCgsMDQ4PEBESExFbTlNBbGlnbm1lbnRaTlNUYWJTdG9wc18QD05TTWluTGluZUhlaWdodFtOU1RleHRMaXN0c1YkY2xhc3NfEA9OU01heExpbmVIZWlnaHQQBIAAI0A8AAAAAAAAgAKABNIVDRYXWk5TLm9iamVjdHOggAPSGRobHFokY2xhc3NuYW1lWCRjbGFzc2VzV05TQXJyYXmiGx1YTlNPYmplY3TSGRofIF8QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxlox8hHV8QEE5TUGFyYWdyYXBoU3R5bGVfEA9OU0tleWVkQXJjaGl2ZXLRJCVUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAFAAXABnAHkAhQCMAJ4AoACiAKsArQCvALQAvwDAAMIAxwDSANsA4wDmAO8A9AEOARIBJQE3AToBPwAAAAAAAAIBAAAAAAAAACYAAAAAAAAAAAAAAAAAAAFB"}}}}},{"_class":"sharedStyle","do_objectID":"69ED5B4A-408D-4DCD-A1CB-392D2A7E41BB","name":"Material\/Light\/Body 1 secondary","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"69ED5B4A-408D-4DCD-A1CB-392D2A7E41BB","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNALAAAAAAAAF5Sb2JvdG8tUmVndWxhctIcHR4fWiRjbGFzc25hbWVYJGNsYXNzZXNfEBNOU011dGFibGVEaWN0aW9uYXJ5ox4gIVxOU0RpY3Rpb25hcnlYTlNPYmplY3TSHB0jJF8QEE5TRm9udERlc2NyaXB0b3KiJSFfEBBOU0ZvbnREZXNjcmlwdG9yXxAPTlNLZXllZEFyY2hpdmVy0SgpVHJvb3SAAQAIABEAGgAjAC0AMgA3AEEARwBMAFMAcAByAHQAewCDAI4AkQCTAJUAmACaAJwAngC0AMoA0wDiAOcA8gD7AREBFQEiASsBMAFDAUYBWQFrAW4BcwAAAAAAAAIBAAAAAAAAACoAAAAAAAAAAAAAAAAAAAF1"},"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NPEBMwIDAgMCAwLjU0Mzg0NjI0MDkAEAGAAtIQERITWiRjbGFzc25hbWVYJGNsYXNzZXNXTlNDb2xvcqISFFhOU09iamVjdF8QD05TS2V5ZWRBcmNoaXZlctEXGFRyb290gAEIERojLTI3O0FITltieHp8gYyVnaCpu77DAAAAAAAAAQEAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAMU="},"NSLigature":0,"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGIiNYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBQYHlUkbnVsbNYJCgsMDQ4PEBESExFbTlNBbGlnbm1lbnRaTlNUYWJTdG9wc18QD05TTWluTGluZUhlaWdodFtOU1RleHRMaXN0c1YkY2xhc3NfEA9OU01heExpbmVIZWlnaHQQBIAAI0A0AAAAAAAAgAKABNIVDRYXWk5TLm9iamVjdHOggAPSGRobHFokY2xhc3NuYW1lWCRjbGFzc2VzV05TQXJyYXmiGx1YTlNPYmplY3TSGRofIF8QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxlox8hHV8QEE5TUGFyYWdyYXBoU3R5bGVfEA9OU0tleWVkQXJjaGl2ZXLRJCVUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAFAAXABnAHkAhQCMAJ4AoACiAKsArQCvALQAvwDAAMIAxwDSANsA4wDmAO8A9AEOARIBJQE3AToBPwAAAAAAAAIBAAAAAAAAACYAAAAAAAAAAAAAAAAAAAFB"}}}}},{"_class":"sharedStyle","do_objectID":"AB229DCC-1C49-4B7D-8079-F9BA9EBB787A","name":"Material\/Desktop\/Light\/Subhead","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"AB229DCC-1C49-4B7D-8079-F9BA9EBB787A","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNALgAAAAAAAF5Sb2JvdG8tUmVndWxhctIcHR4fWiRjbGFzc25hbWVYJGNsYXNzZXNfEBNOU011dGFibGVEaWN0aW9uYXJ5ox4gIVxOU0RpY3Rpb25hcnlYTlNPYmplY3TSHB0jJF8QEE5TRm9udERlc2NyaXB0b3KiJSFfEBBOU0ZvbnREZXNjcmlwdG9yXxAPTlNLZXllZEFyY2hpdmVy0SgpVHJvb3SAAQAIABEAGgAjAC0AMgA3AEEARwBMAFMAcAByAHQAewCDAI4AkQCTAJUAmACaAJwAngC0AMoA0wDiAOcA8gD7AREBFQEiASsBMAFDAUYBWQFrAW4BcwAAAAAAAAIBAAAAAAAAACoAAAAAAAAAAAAAAAAAAAF1"},"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NLMCAwIDAgMC44NwAQAYAC0hAREhNaJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0NvbG9yohIUWE5TT2JqZWN0XxAPTlNLZXllZEFyY2hpdmVy0RcYVHJvb3SAAQgRGiMtMjc7QUhOW2JucHJ3gouTlp+xtLkAAAAAAAABAQAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAuw=="},"NSLigature":0,"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGIiNYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBQYHlUkbnVsbNYJCgsMDQ4PEBESExFbTlNBbGlnbm1lbnRaTlNUYWJTdG9wc18QD05TTWluTGluZUhlaWdodFtOU1RleHRMaXN0c1YkY2xhc3NfEA9OU01heExpbmVIZWlnaHQQBIAAI0A0AAAAAAAAgAKABNIVDRYXWk5TLm9iamVjdHOggAPSGRobHFokY2xhc3NuYW1lWCRjbGFzc2VzV05TQXJyYXmiGx1YTlNPYmplY3TSGRofIF8QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxlox8hHV8QEE5TUGFyYWdyYXBoU3R5bGVfEA9OU0tleWVkQXJjaGl2ZXLRJCVUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAFAAXABnAHkAhQCMAJ4AoACiAKsArQCvALQAvwDAAMIAxwDSANsA4wDmAO8A9AEOARIBJQE3AToBPwAAAAAAAAIBAAAAAAAAACYAAAAAAAAAAAAAAAAAAAFB"}}}}},{"_class":"sharedStyle","do_objectID":"E565A9CE-412B-4E07-B11D-9E9DB95D0595","name":"Material\/Light\/Title","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"E565A9CE-412B-4E07-B11D-9E9DB95D0595","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNANAAAAAAAAF1Sb2JvdG8tTWVkaXVt0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc18QE05TTXV0YWJsZURpY3Rpb25hcnmjHiAhXE5TRGljdGlvbmFyeVhOU09iamVjdNIcHSMkXxAQTlNGb250RGVzY3JpcHRvcqIlIV8QEE5TRm9udERlc2NyaXB0b3JfEA9OU0tleWVkQXJjaGl2ZXLRKClUcm9vdIABAAgAEQAaACMALQAyADcAQQBHAEwAUwBwAHIAdAB7AIMAjgCRAJMAlQCYAJoAnACeALQAygDTAOEA5gDxAPoBEAEUASEBKgEvAUIBRQFYAWoBbQFyAAAAAAAAAgEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAAXQ="},"NSKern":0,"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NLMCAwIDAgMC44NwAQAYAC0hAREhNaJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0NvbG9yohIUWE5TT2JqZWN0XxAPTlNLZXllZEFyY2hpdmVy0RcYVHJvb3SAAQgRGiMtMjc7QUhOW2JucHJ3gouTlp+xtLkAAAAAAAABAQAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAuw=="},"NSLigature":0,"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGIiNYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBQYHlUkbnVsbNYJCgsMDQ4PEBESExFbTlNBbGlnbm1lbnRaTlNUYWJTdG9wc18QD05TTWluTGluZUhlaWdodFtOU1RleHRMaXN0c1YkY2xhc3NfEA9OU01heExpbmVIZWlnaHQQBIAAI0A8AAAAAAAAgAKABNIVDRYXWk5TLm9iamVjdHOggAPSGRobHFokY2xhc3NuYW1lWCRjbGFzc2VzV05TQXJyYXmiGx1YTlNPYmplY3TSGRofIF8QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxlox8hHV8QEE5TUGFyYWdyYXBoU3R5bGVfEA9OU0tleWVkQXJjaGl2ZXLRJCVUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAFAAXABnAHkAhQCMAJ4AoACiAKsArQCvALQAvwDAAMIAxwDSANsA4wDmAO8A9AEOARIBJQE3AToBPwAAAAAAAAIBAAAAAAAAACYAAAAAAAAAAAAAAAAAAAFB"}}}}},{"_class":"sharedStyle","do_objectID":"12DAF224-DCC7-475F-B21D-E6E75E563E9A","name":"Material\/Light\/Subhead secondary","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"12DAF224-DCC7-475F-B21D-E6E75E563E9A","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAMAAAAAAAAF5Sb2JvdG8tUmVndWxhctIcHR4fWiRjbGFzc25hbWVYJGNsYXNzZXNfEBNOU011dGFibGVEaWN0aW9uYXJ5ox4gIVxOU0RpY3Rpb25hcnlYTlNPYmplY3TSHB0jJF8QEE5TRm9udERlc2NyaXB0b3KiJSFfEBBOU0ZvbnREZXNjcmlwdG9yXxAPTlNLZXllZEFyY2hpdmVy0SgpVHJvb3SAAQAIABEAGgAjAC0AMgA3AEEARwBMAFMAcAByAHQAewCDAI4AkQCTAJUAmACaAJwAngC0AMoA0wDiAOcA8gD7AREBFQEiASsBMAFDAUYBWQFrAW4BcwAAAAAAAAIBAAAAAAAAACoAAAAAAAAAAAAAAAAAAAF1"},"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGICFYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBIWHFUkbnVsbNUJCgsMDQ4PEBEQWk5TVGFiU3RvcHNbTlNUZXh0TGlzdHNfEA9OU01pbkxpbmVIZWlnaHRWJGNsYXNzXxAPTlNNYXhMaW5lSGVpZ2h0gACAAiNAOAAAAAAAAIAE0hMMFBVaTlMub2JqZWN0c6CAA9IXGBkaWiRjbGFzc25hbWVYJGNsYXNzZXNXTlNBcnJheaIZG1hOU09iamVjdNIXGB0eXxAXTlNNdXRhYmxlUGFyYWdyYXBoU3R5bGWjHR8bXxAQTlNQYXJhZ3JhcGhTdHlsZV8QD05TS2V5ZWRBcmNoaXZlctEiI1Ryb290gAEACAARABoAIwAtADIANwA9AEMATgBZAGUAdwB+AJAAkgCUAJ0AnwCkAK8AsACyALcAwgDLANMA1gDfAOQA\/gECARUBJwEqAS8AAAAAAAACAQAAAAAAAAAkAAAAAAAAAAAAAAAAAAABMQ=="},"NSLigature":0,"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NPEBMwIDAgMCAwLjU0MTMyNjk5MjgAEAGAAtIQERITWiRjbGFzc25hbWVYJGNsYXNzZXNXTlNDb2xvcqISFFhOU09iamVjdF8QD05TS2V5ZWRBcmNoaXZlctEXGFRyb290gAEIERojLTI3O0FITltieHp8gYyVnaCpu77DAAAAAAAAAQEAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAMU="}}}}},{"_class":"sharedStyle","do_objectID":"9779A83F-D1E9-4FFE-800D-8152982B444C","name":"Material\/Light\/Subhead","value":{"_class":"style","endDecorationType":0,"miterLimit":10,"sharedObjectID":"9779A83F-D1E9-4FFE-800D-8152982B444C","startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAMAAAAAAAAF5Sb2JvdG8tUmVndWxhctIcHR4fWiRjbGFzc25hbWVYJGNsYXNzZXNfEBNOU011dGFibGVEaWN0aW9uYXJ5ox4gIVxOU0RpY3Rpb25hcnlYTlNPYmplY3TSHB0jJF8QEE5TRm9udERlc2NyaXB0b3KiJSFfEBBOU0ZvbnREZXNjcmlwdG9yXxAPTlNLZXllZEFyY2hpdmVy0SgpVHJvb3SAAQAIABEAGgAjAC0AMgA3AEEARwBMAFMAcAByAHQAewCDAI4AkQCTAJUAmACaAJwAngC0AMoA0wDiAOcA8gD7AREBFQEiASsBMAFDAUYBWQFrAW4BcwAAAAAAAAIBAAAAAAAAACoAAAAAAAAAAAAAAAAAAAF1"},"NSColor":{"_archive":"YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OVU5TUkdCXE5TQ29sb3JTcGFjZVYkY2xhc3NLMCAwIDAgMC44NwAQAYAC0hAREhNaJGNsYXNzbmFtZVgkY2xhc3Nlc1dOU0NvbG9yohIUWE5TT2JqZWN0XxAPTlNLZXllZEFyY2hpdmVy0RcYVHJvb3SAAQgRGiMtMjc7QUhOW2JucHJ3gouTlp+xtLkAAAAAAAABAQAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAuw=="},"NSLigature":0,"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGIiNYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKUHCBQYHlUkbnVsbNYJCgsMDQ4PEBESExFbTlNBbGlnbm1lbnRaTlNUYWJTdG9wc18QD05TTWluTGluZUhlaWdodFtOU1RleHRMaXN0c1YkY2xhc3NfEA9OU01heExpbmVIZWlnaHQQBIAAI0A4AAAAAAAAgAKABNIVDRYXWk5TLm9iamVjdHOggAPSGRobHFokY2xhc3NuYW1lWCRjbGFzc2VzV05TQXJyYXmiGx1YTlNPYmplY3TSGRofIF8QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxlox8hHV8QEE5TUGFyYWdyYXBoU3R5bGVfEA9OU0tleWVkQXJjaGl2ZXLRJCVUcm9vdIABAAgAEQAaACMALQAyADcAPQBDAFAAXABnAHkAhQCMAJ4AoACiAKsArQCvALQAvwDAAMIAxwDSANsA4wDmAO8A9AEOARIBJQE3AToBPwAAAAAAAAIBAAAAAAAAACYAAAAAAAAAAAAAAAAAAAFB"}}}}}]},"pages":[{"_class":"MSJSONFileReference","_ref_class":"MSImmutablePage","_ref":"pages\/226D6615-C16F-4B84-92E0-291B2F1B15C4"},{"_class":"MSJSONFileReference","_ref_class":"MSImmutablePage","_ref":"pages\/1B67083D-3430-4865-A36A-6C687A1EEB45"}]}
================================================
FILE: design-files/Moderator-StickerSheet-20161117-DAS/meta.json
================================================
{"commit":"335a30073fcb2dc64a0abd6148ae147d694c887d","appVersion":"43.1","build":39012,"app":"com.bohemiancoding.sketch3","pagesAndArtboards":{"226D6615-C16F-4B84-92E0-291B2F1B15C4":{"name":"Page 1","artboards":{"13915B01-7A46-4D0E-BFEA-D8170EF6C531":{"name":"Tablet 9inch"}}},"1B67083D-3430-4865-A36A-6C687A1EEB45":{"name":"Symbols","artboards":{"DDB0E68B-BEEE-4E3A-9844-0443F2E09696":{"name":"Material\/Icons black\/refresh"},"34CA2EB5-EBA4-4D7A-9C5B-1EC39EA97E4F":{"name":"Material\/Icons black\/close"},"57C2B1F6-1718-4BD9-85D3-39BEFA28613B":{"name":"Material\/Icons white\/arrow back"},"7C5A0EC0-D963-4207-80CB-49ED115BF9B5":{"name":"Material\/Android\/Navbar 1024dp black"},"A0EAD81F-5486-461D-846C-2016D95CE579":{"name":"Material\/Icons white\/close"},"76C8A3F1-FA9D-4553-9035-374EEF18CBF2":{"name":"Material\/Icons black\/arrow back"},"E33E2440-9E0D-4801-A0C4-5F0D7AA40DE6":{"name":"Material\/Icons white\/more vert"},"10E7654C-E936-403F-9CEF-AA512F39B60F":{"name":"Material\/Icons white\/arrow drop down"},"8E30A0EB-7E44-41FB-B562-98898C86C5CF":{"name":"Material\/Icons black\/arrow drop up"},"D284E6EB-A900-4B52-B3A1-0461AFBE8EFD":{"name":"Material\/Android\/Status bar 1024dp black"},"D11A2A32-7CF0-4C46-B91D-9FE1B6C7CBB3":{"name":"Material\/Icons white\/search"},"21B63B46-4C5E-4D74-9E80-2B8CCF870C32":{"name":"Material\/Icons black\/check"},"CA799155-0C43-4E3B-8162-8521128A451A":{"name":"Material\/Icons black\/more vert"},"3D9F5A8F-3429-4C85-881D-96C7B13601E9":{"name":"Material\/Android\/Status bar content light"}}}},"fonts":["ITCFranklinGothicStd-Med","ITCFranklinGothicStd-Book","ITCFranklinGothicStd-Hvy",".SFNSText","CheltenhamStd-Book","ITCFranklinGothicStd-Demi","Roboto-Medium","Cheltenham-Book"],"created":{"app":"com.bohemiancoding.sketch3","commit":"566dab740f05650c87722a04bbcb154884097f92","version":87,"appVersion":"42","variant":"NONAPPSTORE","build":36781},"version":88,"saveHistory":["NONAPPSTORE.36781","NONAPPSTORE.39012"],"autosaved":0,"variant":"NONAPPSTORE"}
================================================
FILE: design-files/Moderator-StickerSheet-20161117-DAS/pages/1B67083D-3430-4865-A36A-6C687A1EEB45.json
================================================
{"_class":"page","do_objectID":"1B67083D-3430-4865-A36A-6C687A1EEB45","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":300,"width":300,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Symbols","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"symbolMaster","do_objectID":"3D9F5A8F-3429-4C85-881D-96C7B13601E9","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":118,"x":100,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Android\/Status bar content light","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","do_objectID":"B99E6927-021E-45DC-B685-DA1D14859377","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"group","do_objectID":"07EA6ABD-426B-469E-85BA-0D2542F1EB5B","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":36,"x":74,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"time","nameIsFixed":true,"originalObjectID":"07EA6ABD-426B-469E-85BA-0D2542F1EB5B","resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":0.9},"endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"text","do_objectID":"2851D932-9391-44B6-BA6A-55668EC918E6","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":36,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"12:30","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0,"textStyle":{"_class":"textStyle","encodedAttributes":{"NSLigature":0,"MSAttributedStringFontAttribute":{"_archive":"YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNALAAAAAAAAF1Sb2JvdG8tTWVkaXVt0hwdHh9aJGNsYXNzbmFtZVgkY2xhc3Nlc18QE05TTXV0YWJsZURpY3Rpb25hcnmjHiAhXE5TRGljdGlvbmFyeVhOU09iamVjdNIcHSMkXxAQTlNGb250RGVzY3JpcHRvcqIlIV8QEE5TRm9udERlc2NyaXB0b3JfEA9OU0tleWVkQXJjaGl2ZXLRKClUcm9vdIABAAgAEQAaACMALQAyADcAQQBHAEwAUwBwAHIAdAB7AIMAjgCRAJMAlQCYAJoAnACeALQAygDTAOEA5gDxAPoBEAEUASEBKgEvAUIBRQFYAWoBbQFyAAAAAAAAAgEAAAAAAAAAKgAAAAAAAAAAAAAAAAAAAXQ="},"NSParagraphStyle":{"_archive":"YnBsaXN0MDDUAQIDBAUGFhdYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OWk5TVGFiU3RvcHNWJGNsYXNzW05TQWxpZ25tZW50gACAAhAB0hAREhNaJGNsYXNzbmFtZVgkY2xhc3Nlc18QF05TTXV0YWJsZVBhcmFncmFwaFN0eWxloxIUFV8QEE5TUGFyYWdyYXBoU3R5bGVYTlNPYmplY3RfEA9OU0tleWVkQXJjaGl2ZXLRGBlUcm9vdIABCBEaIy0yNztBSFNaZmhqbHF8hZ+jtr\/R1NkAAAAAAAABAQAAAAAAAAAaAAAAAAAAAAAAAAAAAAAA2w=="}}}},"attributedString":{"_class":"MSAttributedString","archivedAttributedString":{"_archive":"YnBsaXN0MDDUAQIDBAUGS0xYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK8QFAcIDxAcHR4fICQsLS4vMDc7QUVHVSRudWxs0wkKCwwNDlhOU1N0cmluZ1YkY2xhc3NcTlNBdHRyaWJ1dGVzgAKAE4ADVTEyOjMw0xESChMXG1dOUy5rZXlzWk5TLm9iamVjdHOjFBUWgASABYAGoxgZGoAHgAiAEIASWk5TTGlnYXR1cmVfEB9NU0F0dHJpYnV0ZWRTdHJpbmdGb250QXR0cmlidXRlXxAQTlNQYXJhZ3JhcGhTdHlsZRAA0gohIiNfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4APgAnTERIKJSgroiYngAqAC6IpKoAMgA2ADl8QE05TRm9udFNpemVBdHRyaWJ1dGVfEBNOU0ZvbnROYW1lQXR0cmlidXRlI0AsAAAAAAAAXVJvYm90by1NZWRpdW3SMTIzNFokY2xhc3NuYW1lWCRjbGFzc2VzXxATTlNNdXRhYmxlRGljdGlvbmFyeaMzNTZcTlNEaWN0aW9uYXJ5WE5TT2JqZWN00jEyODlfEBBOU0ZvbnREZXNjcmlwdG9yojo2XxAQTlNGb250RGVzY3JpcHRvctM8Cj0+P0BaTlNUYWJTdG9wc1tOU0FsaWdubWVudIAAgBEQAdIxMkJDXxAXTlNNdXRhYmxlUGFyYWdyYXBoU3R5bGWjQkQ2XxAQTlNQYXJhZ3JhcGhTdHlsZdIxMjVGojU20jEySElfEBJOU0F0dHJpYnV0ZWRTdHJpbmeiSjZfEBJOU0F0dHJpYnV0ZWRTdHJpbmdfEA9OU0tleWVkQXJjaGl2ZXLRTU5Ucm9vdIABAAgAEQAaACMALQAyADcATgBUAFsAZABrAHgAegB8AH4AhACLAJMAngCiAKQApgCoAKwArgCwALIAtAC\/AOEA9AD2APsBGAEaARwBIwEmASgBKgEtAS8BMQEzAUkBXwFoAXYBewGGAY8BpQGpAbYBvwHEAdcB2gHtAfQB\/wILAg0CDwIRAhYCMAI0AkcCTAJPAlQCaQJsAoECkwKWApsAAAAAAAACAQAAAAAAAABPAAAAAAAAAAAAAAAAAAACnQ=="}},"automaticallyDrawOnUnderlyingPath":false,"dontSynchroniseWithSymbol":false,"glyphBounds":"{{0, 0}, {36, 16.40625}}","heightIsClipped":false,"lineSpacingBehaviour":0,"textBehaviour":0}]},{"_class":"group","do_objectID":"42D99C57-EF76-4D8A-928A-44B249108E0B","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":16,"width":16,"x":55,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"battery","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapeGroup","do_objectID":"948320AB-B4A4-4749-8445-86978E2DC560","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"bounds","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"79A9EE07-A92C-4BE1-BE3A-2762F4D51368","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"shapeGroup","do_objectID":"FE57A042-EDF4-4C50-82B0-380EF7B28AC3","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":9,"x":3,"y":1},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"9DEEA577-F6E6-47F4-8439-C2A4A0045478","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":9,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.72222222222222221, 0.12337662337662333}","curveMode":1,"curveTo":"{0.72222222222222221, 0.12337662337662333}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.66666666666666663, 0.062500000000000014}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.72222222222222221, 2.6404969480761258e-16}","curveMode":1,"curveTo":"{0.72222222222222221, 2.6404969480761258e-16}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.66666666666666663, 2.6962559169468085e-16}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.33333333333333331, 7.9301644616082606e-18}","curveMode":1,"curveTo":"{0.33333333333333331, 7.9301644616082606e-18}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.33333333333333331, 7.9301644616082606e-18}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.33333333333333331, 0.12337662337662332}","curveMode":1,"curveTo":"{0.33333333333333331, 0.12337662337662332}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.33333333333333331, 0.0625}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.1194805194805194}","curveMode":1,"curveTo":"{0, 0.047402597402597425}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.0625}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.047402597402597377}","curveMode":1,"curveTo":"{1, 0.11948051948051941}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.0625}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}]},{"_class":"group","do_objectID":"3DA9B3DF-5C04-4A39-9C53-9969BD080CF8","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":16,"width":16,"x":35,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"cellular","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapeGroup","do_objectID":"9CE34C1D-0979-49AC-9C7A-5CC1C8AB236B","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"bounds","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"167CE758-4A23-4C76-9056-9B8685747098","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"shapeGroup","do_objectID":"772A4B91-3052-43B7-9601-9EEC2B2FEAE6","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":14,"width":14,"x":0,"y":1},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"224429C0-2948-4BCB-8BEF-1562B4041907","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":14,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}]},{"_class":"group","do_objectID":"A33967C6-E840-423D-ABC0-352996D1DAEC","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":16,"width":20,"x":14,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"wifi","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapeGroup","do_objectID":"DA3E3AFB-0BCC-4441-8062-925273087AF6","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":2,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"bounds","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"D15D00AC-B194-4DEB-807C-B29A7A0C30D1","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"shapeGroup","do_objectID":"9DCE5055-9A08-4B23-B1D5-943FB838D12C","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":14,"width":18.0452558296814,"x":0.9773720851803773,"y":1},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"oval","do_objectID":"17C69D78-7C2A-46AF-B6C5-2106D448D51A","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":30,"width":30,"x":-5.9773720851804,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":false,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77614237490000004, 1}","curveMode":2,"curveTo":"{0.22385762510000001, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.22385762510000001}","curveMode":2,"curveTo":"{1, 0.77614237490000004}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22385762510000001, 0}","curveMode":2,"curveTo":"{0.77614237490000004, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77614237490000004}","curveMode":2,"curveTo":"{0, 0.22385762510000001}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"}]}},{"_class":"shapePath","do_objectID":"AC4876A8-B88A-47AD-A48E-3023127CD30D","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":23,"x":-2.4773720851804,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path 13","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":2,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.13043478260869393, 1.0000000000000004}","curveMode":1,"curveTo":"{0.13043478260869393, 1.0000000000000004}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.50000000000000067, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{3.8616453030440226e-17, 0}","curveMode":1,"curveTo":"{3.8616453030440226e-17, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{3.8616453030440226e-17, 0}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}]}],"backgroundColor":{"_class":"color","alpha":1,"blue":0.2588235294117647,"green":0.2588235294117647,"red":0.2588235294117647},"hasBackgroundColor":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"080C0EF9-47BD-4900-BB91-42C80173D424"},{"_class":"symbolMaster","do_objectID":"7C5A0EC0-D963-4207-80CB-49ED115BF9B5","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":48,"width":1024,"x":318,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Android\/Navbar 1024dp black","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"FD276149-9FFE-47EE-AFEF-6E94CAAB15AB","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":48,"width":1024,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"navbar bg","nameIsFixed":true,"originalObjectID":"FD276149-9FFE-47EE-AFEF-6E94CAAB15AB","resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"rectangle","do_objectID":"93E17AB9-FD66-4372-A444-CA8F05F091D4","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":48,"width":1024,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Rectangle-path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":false,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"}]},"fixedRadius":0,"hasConvertedToNewRoundCorners":true}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"shapeGroup","do_objectID":"C32AB187-22D2-4FD4-A756-372306E00456","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":16,"width":16,"x":709,"y":16},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"recent","nameIsFixed":true,"originalObjectID":"C32AB187-22D2-4FD4-A756-372306E00456","resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","borders":[{"_class":"border","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"position":1,"thickness":2}],"endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"rectangle","do_objectID":"C05F4D69-B880-485E-9DEA-0445FDB5CEBE","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":false,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"},{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"}]},"fixedRadius":2,"hasConvertedToNewRoundCorners":true}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"shapeGroup","do_objectID":"CA1CFE60-94CC-4DFE-ADB7-228099132327","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":true,"height":16,"width":16,"x":504.9975292549875,"y":16.00097492402438},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"home","nameIsFixed":true,"originalObjectID":"CA1CFE60-94CC-4DFE-ADB7-228099132327","resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","borders":[{"_class":"border","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"position":1,"thickness":2}],"endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"oval","do_objectID":"8873C58B-0DDD-45A4-8A28-EC4E572620DF","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":false,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77614237490000004, 1}","curveMode":2,"curveTo":"{0.22385762510000001, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.22385762510000001}","curveMode":2,"curveTo":"{1, 0.77614237490000004}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22385762510000001, 0}","curveMode":2,"curveTo":"{0.77614237490000004, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77614237490000004}","curveMode":2,"curveTo":"{0, 0.22385762510000001}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"shapeGroup","do_objectID":"B139CB06-4803-4C60-9112-3DEE3F51D109","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":17.45461322817407,"width":14.99505859673756,"x":301.0024707450125,"y":15.27269338591304},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"back","nameIsFixed":true,"originalObjectID":"B139CB06-4803-4C60-9112-3DEE3F51D109","resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","borders":[{"_class":"border","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"position":1,"thickness":2}],"endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapePath","do_objectID":"466F6847-C95A-4EAD-A447-C48DCC7870DC","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":17.45461322817407,"width":14.99505859673756,"x":0,"y":1.77635683940025e-15},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{0.99999997746672664, -0.044212864809782863}","curveMode":1,"curveTo":"{0.99999997746672664, -0.044212864809782863}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.99999997746672664, -0.044212864809782863}"},{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{0.72049598702496187, 1.1868950936476208}","curveMode":1,"curveTo":"{1.2795040182693471, 0.90175412939323807}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1.0000000026471545, 1.0443246115204294}"},{"_class":"curvePoint","cornerRadius":2,"curveFrom":"{-0.067018176924187017, 0.5000558548053492}","curveMode":1,"curveTo":"{-0.067018176924187017, 0.5000558548053492}","hasCurveFrom":false,"hasCurveTo":false,"point":"{-0.067018176924187017, 0.5000558548053492}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"ED9B7E91-942C-4237-9FBB-46015072DC87"},{"_class":"symbolMaster","do_objectID":"E33E2440-9E0D-4801-A0C4-5F0D7AA40DE6","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":12,"x":1442,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons white\/more vert","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","do_objectID":"80066115-E9C9-49E6-A544-44ED048B1155","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"81A456AC-794A-4C12-82B7-424EEBB853DA","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"4E763800-4F35-4D88-BD54-27F2228FFD7B","constrainProportions":false,"height":16,"width":4,"x":4,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":true,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"78B24937-2B1B-4AC0-8304-D12FF139276F","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"A93742BB-00F9-4F61-A4D3-18497765722F","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"4CC0D4E9-894A-4BCE-9A28-86E02BC355DD","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":4,"width":4,"x":0,"y":12},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77499999999999991, 0}","curveMode":4,"curveTo":"{0.5, 0}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.77499999999999991}","curveMode":2,"curveTo":"{1, 0.22500000000000009}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22500000000000009, 1}","curveMode":2,"curveTo":"{0.77499999999999991, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.22500000000000009}","curveMode":2,"curveTo":"{0, 0.77499999999999991}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0}","curveMode":4,"curveTo":"{0.22500000000000009, 0}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 0}"}]}},{"_class":"shapePath","do_objectID":"19FCD26B-6C74-46C6-95AF-4FBD48018DFA","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":4,"width":4,"x":0,"y":6},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22500000000000009, 1}","curveMode":4,"curveTo":"{0.5, 1}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.22500000000000009}","curveMode":2,"curveTo":"{0, 0.77499999999999991}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77499999999999991, 0}","curveMode":2,"curveTo":"{0.22500000000000009, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.77499999999999991}","curveMode":2,"curveTo":"{1, 0.22500000000000009}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 1}","curveMode":4,"curveTo":"{0.77499999999999991, 1}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 1}"}]}},{"_class":"shapePath","do_objectID":"14AD854B-BD0A-4BA8-B581-7ECE76C1CA08","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":4,"width":4,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22500000000000009, 1}","curveMode":4,"curveTo":"{0.5, 1}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.22499999999999964}","curveMode":2,"curveTo":"{0, 0.77500000000000036}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77499999999999991, 0}","curveMode":2,"curveTo":"{0.22500000000000009, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.77500000000000036}","curveMode":2,"curveTo":"{1, 0.22499999999999964}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 1}","curveMode":4,"curveTo":"{0.77499999999999991, 1}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 1}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":0.2588235294117647,"green":0.2588235294117647,"red":0.2588235294117647},"hasBackgroundColor":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"0E94AECA-E862-48AE-9B52-257AAE09FC3A"},{"_class":"symbolMaster","do_objectID":"57C2B1F6-1718-4BD9-85D3-39BEFA28613B","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":24,"x":1554,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons white\/arrow back","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","do_objectID":"24C96E56-1A8A-4A56-B78D-E65C53111097","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"E5F819E4-BE46-40EB-B37C-15B389D007FE","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":15.99999999999994,"x":3.700000000000045,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"5A6AFA73-58B5-4893-9E5C-F87AD791FC88","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"A93742BB-00F9-4F61-A4D3-18497765722F","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"7364F9DB-12C9-4030-BA13-488063868527","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":15.99999999999994,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.4375}","curveMode":1,"curveTo":"{1, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.4375}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.23749999999999799, 0.4375}","curveMode":1,"curveTo":"{0.23749999999999799, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.23749999999999799, 0.4375}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.58750000000000069, 0.087499999999998579}","curveMode":1,"curveTo":"{0.58750000000000069, 0.087499999999998579}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.58750000000000069, 0.087499999999998579}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.50000000000000178, 0}","curveMode":1,"curveTo":"{0.50000000000000178, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.50000000000000178, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.5}","curveMode":1,"curveTo":"{0, 0.5}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.50000000000000178, 1}","curveMode":1,"curveTo":"{0.50000000000000178, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.50000000000000178, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.58750000000000069, 0.91250000000000142}","curveMode":1,"curveTo":"{0.58750000000000069, 0.91250000000000142}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.58750000000000069, 0.91250000000000142}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.23749999999999799, 0.5625}","curveMode":1,"curveTo":"{0.23749999999999799, 0.5625}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.23749999999999799, 0.5625}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.5625}","curveMode":1,"curveTo":"{1, 0.5625}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.5625}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.4375}","curveMode":1,"curveTo":"{1, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.4375}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":0.2588235294117647,"green":0.2588235294117647,"red":0.2588235294117647},"hasBackgroundColor":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"399C1B51-205C-4A6A-B53D-5715C2003838"},{"_class":"symbolMaster","do_objectID":"D284E6EB-A900-4B52-B3A1-0461AFBE8EFD","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":1024,"x":1678,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Material\/Android\/Status bar 1024dp black","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"8C2C177A-6BDB-4A27-AED7-BFD6E8C28CFC","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":1024,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"status bar bg","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"rectangle","do_objectID":"1A534E53-529F-43A2-9E31-1B2F30F8B3F9","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":1024,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Rectangle-path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":false,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"}]},"fixedRadius":0,"hasConvertedToNewRoundCorners":true}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1},{"_class":"symbolInstance","do_objectID":"47930CB2-9DE7-4E64-B046-ED7FCD4A8102","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":118,"x":906,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"status bar content","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"horizontalSpacing":0,"masterInfluenceEdgeMaxXPadding":0,"masterInfluenceEdgeMaxYPadding":11,"masterInfluenceEdgeMinXPadding":0,"masterInfluenceEdgeMinYPadding":0,"symbolID":"080C0EF9-47BD-4900-BB91-42C80173D424","verticalSpacing":0,"overrides":{"0":{}}}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"88160964-34E8-4518-8C33-9A5CE739F4A7"},{"_class":"symbolMaster","do_objectID":"D11A2A32-7CF0-4C46-B91D-9FE1B6C7CBB3","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"620DB548-1D66-401B-B650-ADCC059489A4","constrainProportions":false,"height":24,"width":24,"x":2802,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Material\/Icons white\/search","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","do_objectID":"1EC8BD03-31CD-4BC9-B0E7-DE236CF32471","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"B11F8503-58EE-4781-AD1E-23038435CBB8","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"4D3B3EC6-B673-4CA9-B6CD-EDD0BDA8E718","constrainProportions":false,"height":17.491,"width":17.49,"x":3,"y":3},"isFlippedHorizontal":false,"isFlippedVertical":true,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"F1F9F873-3F66-43CC-BF1C-30FC1724183E","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"A93742BB-00F9-4F61-A4D3-18497765722F","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"D72E3724-172E-4777-AEC1-7E2FFBAF73CC","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"E9107034-A95A-4613-B57D-EC674C4C4836","constrainProportions":false,"height":17.491,"width":17.49,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":true,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.7148084619782733, 0.62889486021382424}","curveMode":1,"curveTo":"{0.7148084619782733, 0.62889486021382424}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.7148084619782733, 0.62889486021382424}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.66941109205260141, 0.62889486021382424}","curveMode":1,"curveTo":"{0.66941109205260141, 0.62889486021382424}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.66941109205260141, 0.62889486021382424}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.70937678673527726, 0.5482248013263965}","curveMode":4,"curveTo":"{0.65363064608347621, 0.61322966096849807}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.65363064608347621, 0.61322966096849807}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.74328187535734702, 0.16637127665656626}","curveMode":3,"curveTo":"{0.74328187535734702, 0.46395289005774398}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.74328187535734702, 0.37161969012635071}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.16638078902229844, 0}","curveMode":2,"curveTo":"{0.57690108633504855, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.37164093767867351, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.57686810359613516}","curveMode":2,"curveTo":"{0, 0.16637127665656626}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.37161969012635071}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.46397941680960547, 0.74323938025270142}","curveMode":3,"curveTo":"{0.16638078902229844, 0.74323938025270142}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.37164093767867351, 0.74323938025270142}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.61320754716981118, 0.65370762106226066}","curveMode":4,"curveTo":"{0.5481989708404803, 0.70939340232119374}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.61320754716981118, 0.65370762106226066}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.62898799313893639, 0.66937282030758682}","curveMode":1,"curveTo":"{0.62898799313893639, 0.66937282030758682}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.62898799313893639, 0.66937282030758682}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.62898799313893639, 0.7146532502429821}","curveMode":1,"curveTo":"{0.62898799313893639, 0.7146532502429821}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.62898799313893639, 0.7146532502429821}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.91475128644939963, 1}","curveMode":1,"curveTo":"{0.91475128644939963, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.91475128644939963, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.91475616031101714}","curveMode":1,"curveTo":"{1, 0.91475616031101714}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.91475616031101714}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.7148084619782733, 0.62889486021382424}","curveMode":1,"curveTo":"{0.7148084619782733, 0.62889486021382424}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.7148084619782733, 0.62889486021382424}"}]}},{"_class":"shapePath","do_objectID":"FA3610BD-4C4F-47C4-B7B8-440CE1981E3A","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"C2BF318E-DE70-4EA3-8ABC-D8A0303D827E","constrainProportions":false,"height":9,"width":9,"x":2,"y":6.490999999999985},"isFlippedHorizontal":false,"isFlippedVertical":true,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.2237777777777778, 1}","curveMode":4,"curveTo":"{0.5, 1}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.22388888888888894}","curveMode":2,"curveTo":"{0, 0.77622222222222226}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77611111111111108, 0}","curveMode":2,"curveTo":"{0.2237777777777778, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.77622222222222226}","curveMode":2,"curveTo":"{1, 0.22388888888888894}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 1}","curveMode":4,"curveTo":"{0.77611111111111108, 1}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 1}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":0.2588235294117647,"green":0.2588235294117647,"red":0.2588235294117647},"hasBackgroundColor":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"0D519726-DC4C-4BF3-8DA2-C387E9DF606E"},{"_class":"symbolMaster","do_objectID":"10E7654C-E936-403F-9CEF-AA512F39B60F","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":24,"x":2926,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons white\/arrow drop down","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"57F766A9-1E61-4A19-9FC8-B5C38370035E","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":5,"width":10,"x":7,"y":10},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"A93742BB-00F9-4F61-A4D3-18497765722F","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"A65159E5-B368-48D8-AAD4-6143D1C687F7","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":5,"width":10,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 1}","curveMode":1,"curveTo":"{0.5, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":0.2588235294117647,"green":0.2588235294117647,"red":0.2588235294117647},"hasBackgroundColor":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"13DF2748-134F-4932-AFF1-A04B20BCEB5D"},{"_class":"symbolMaster","do_objectID":"8E30A0EB-7E44-41FB-B562-98898C86C5CF","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"E92EB70E-0A8F-47D2-A9FC-AE07CFC84D88","constrainProportions":false,"height":24,"width":24,"x":3050,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons black\/arrow drop up","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"C9FDF8B1-CADE-47D0-BA77-F1699B31B101","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"7AFE403B-7BFC-4AC0-95E6-8CB29E41EE9E","constrainProportions":false,"height":5,"width":10,"x":7,"y":9},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"66D2FB6D-BAF8-4D74-A80B-82B160E929FF","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"C941E34F-358F-403D-8D24-FC0377A5CF66","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":5,"width":10,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0}","curveMode":1,"curveTo":"{0.5, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"FB5EAE15-93F8-464A-90D4-ED01D9F6980B"},{"_class":"symbolMaster","do_objectID":"A0EAD81F-5486-461D-846C-2016D95CE579","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":24,"x":3174,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons white\/close","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","do_objectID":"F365C82D-DE5F-4804-A574-685B2F36B01D","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"1761D2BD-4632-4D21-92C3-0FC1207AAFCF","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":14,"x":5,"y":5},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"A93742BB-00F9-4F61-A4D3-18497765722F","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"D966142A-CD98-46C9-BDAD-9D321B106E74","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":14,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.099999999999998382}","curveMode":1,"curveTo":"{1, 0.099999999999998382}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.099999999999998382}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.90000000000000158, 0}","curveMode":1,"curveTo":"{0.90000000000000158, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.90000000000000158, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0.40000000000000163}","curveMode":1,"curveTo":"{0.5, 0.40000000000000163}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0.40000000000000163}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.099999999999998382, 0}","curveMode":1,"curveTo":"{0.099999999999998382, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.099999999999998382, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.099999999999998382}","curveMode":1,"curveTo":"{0, 0.099999999999998382}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.099999999999998382}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.40000000000000163, 0.5}","curveMode":1,"curveTo":"{0.40000000000000163, 0.5}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.40000000000000163, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.90000000000000158}","curveMode":1,"curveTo":"{0, 0.90000000000000158}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.90000000000000158}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.099999999999998382, 1}","curveMode":1,"curveTo":"{0.099999999999998382, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.099999999999998382, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0.59999999999999842}","curveMode":1,"curveTo":"{0.5, 0.59999999999999842}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0.59999999999999842}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.90000000000000158, 1}","curveMode":1,"curveTo":"{0.90000000000000158, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.90000000000000158, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.90000000000000158}","curveMode":1,"curveTo":"{1, 0.90000000000000158}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.90000000000000158}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.59999999999999842, 0.5}","curveMode":1,"curveTo":"{0.59999999999999842, 0.5}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.59999999999999842, 0.5}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":0.2588235294117647,"green":0.2588235294117647,"red":0.2588235294117647},"hasBackgroundColor":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"7FD66874-2EE6-4590-BBF7-EE5C8FCEA405"},{"_class":"symbolMaster","do_objectID":"21B63B46-4C5E-4D74-9E80-2B8CCF870C32","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"0896C0E2-B56B-42C7-BF44-C662D526E64D","constrainProportions":false,"height":24,"width":24,"x":3298,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons black\/check","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"80BA1CB4-1207-4D70-A6A4-B01D84837060","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"FA46C3A8-545B-4470-A9E9-59CE80683639","constrainProportions":false,"height":13.39999999999998,"width":17.60000000000002,"x":3.399993896484375,"y":5.599999999999909},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"C501C759-A703-4F90-A2D6-4AB5F3E4AE55","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"2A9AB63B-64CF-4B9E-BE9B-7C9B8EA62134","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"27C107F2-9A34-4531-B77F-B9EFFF0BEEC1","constrainProportions":false,"height":13.39999999999998,"width":17.60000000000002,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.31818181818181906, 0.79104477611940605}","curveMode":1,"curveTo":"{0.31818181818181906, 0.79104477611940605}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.31818181818181906, 0.79104477611940605}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.07954545454545961, 0.47761194029850657}","curveMode":1,"curveTo":"{0.07954545454545961, 0.47761194029850657}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.07954545454545961, 0.47761194029850657}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.58208955223880354}","curveMode":1,"curveTo":"{0, 0.58208955223880354}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.58208955223880354}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.31818181818181906, 1}","curveMode":1,"curveTo":"{0.31818181818181906, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.31818181818181906, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.10447761194029699}","curveMode":1,"curveTo":"{1, 0.10447761194029699}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.10447761194029699}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.92045454545454686, 0}","curveMode":1,"curveTo":"{0.92045454545454686, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.92045454545454686, 0}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"173D909A-EFDA-4D89-BD96-A54732F86FBC"},{"_class":"symbolMaster","do_objectID":"34CA2EB5-EBA4-4D7A-9C5B-1EC39EA97E4F","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":24,"x":3422,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Material\/Icons black\/close","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"0F1B587E-CE5B-4C68-AE12-9259ACBAD47F","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":14,"x":5,"y":5},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"5E67E0D0-732F-4BED-BA12-13E7712F916D","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"792E7CCE-2239-4F83-8C42-B81247D8DB36","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":14,"width":14,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.099999999999998382}","curveMode":1,"curveTo":"{1, 0.099999999999998382}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.099999999999998382}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.90000000000000158, 0}","curveMode":1,"curveTo":"{0.90000000000000158, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.90000000000000158, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0.40000000000000163}","curveMode":1,"curveTo":"{0.5, 0.40000000000000163}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0.40000000000000163}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.099999999999998382, 0}","curveMode":1,"curveTo":"{0.099999999999998382, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.099999999999998382, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.099999999999998382}","curveMode":1,"curveTo":"{0, 0.099999999999998382}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.099999999999998382}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.40000000000000163, 0.5}","curveMode":1,"curveTo":"{0.40000000000000163, 0.5}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.40000000000000163, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.90000000000000158}","curveMode":1,"curveTo":"{0, 0.90000000000000158}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.90000000000000158}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.099999999999998382, 1}","curveMode":1,"curveTo":"{0.099999999999998382, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.099999999999998382, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0.59999999999999842}","curveMode":1,"curveTo":"{0.5, 0.59999999999999842}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0.59999999999999842}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.90000000000000158, 1}","curveMode":1,"curveTo":"{0.90000000000000158, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.90000000000000158, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.90000000000000158}","curveMode":1,"curveTo":"{1, 0.90000000000000158}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.90000000000000158}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.59999999999999842, 0.5}","curveMode":1,"curveTo":"{0.59999999999999842, 0.5}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.59999999999999842, 0.5}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"A2EB1EF8-52D0-4370-A4C9-6E97D54C71F5"},{"_class":"symbolMaster","do_objectID":"CA799155-0C43-4E3B-8162-8521128A451A","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":12,"x":3546,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons black\/more vert","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"428FB459-330F-4D52-AE8E-8992B42D0835","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"90C406E0-0192-4A84-9910-606A3A1C3D98","constrainProportions":false,"height":16,"width":4,"x":4,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"768DEAB1-5B2D-4B48-92DD-7E7E8A2175C1","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"3EBF9BF2-E838-4353-B39A-C5A0B2267E83","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":4,"width":4,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77500000000000568, 1}","curveMode":4,"curveTo":"{0.5, 1}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.22499999999999432}","curveMode":2,"curveTo":"{1, 0.77500000000000568}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22499999999999432, 0}","curveMode":2,"curveTo":"{0.77500000000000568, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77500000000000568}","curveMode":2,"curveTo":"{0, 0.22499999999999432}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 1}","curveMode":4,"curveTo":"{0.22499999999999432, 1}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 1}"}]}},{"_class":"shapePath","do_objectID":"E47C78F6-6493-40B8-94F4-05DCB1FED9D2","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":4,"width":4,"x":0,"y":6},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22499999999999432, 0}","curveMode":4,"curveTo":"{0.5, 0}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77500000000000568}","curveMode":2,"curveTo":"{0, 0.22499999999999432}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77500000000000568, 1}","curveMode":2,"curveTo":"{0.22499999999999432, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.22499999999999432}","curveMode":2,"curveTo":"{1, 0.77500000000000568}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0}","curveMode":4,"curveTo":"{0.77500000000000568, 0}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 0}"}]}},{"_class":"shapePath","do_objectID":"5D799E6A-A1E8-440D-8ECE-3353F766E0E3","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":4,"width":4,"x":0,"y":12},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22499999999999432, 0}","curveMode":4,"curveTo":"{0.5, 0}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77500000000000568}","curveMode":2,"curveTo":"{0, 0.22499999999999432}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77500000000000568, 1}","curveMode":2,"curveTo":"{0.22499999999999432, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.22499999999999432}","curveMode":2,"curveTo":"{1, 0.77500000000000568}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0}","curveMode":4,"curveTo":"{0.77500000000000568, 0}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.5, 0}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"CBDCF326-5C6B-4C9C-BAA5-D47108544398"},{"_class":"symbolMaster","do_objectID":"DDB0E68B-BEEE-4E3A-9844-0443F2E09696","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":24,"width":24,"x":3658,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons black\/refresh","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"71365292-C3BA-4844-8627-D09CBFF6B16C","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"31B62D58-947D-4E7E-99F0-DD70B73B987B","constrainProportions":false,"height":16,"width":16,"x":4,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"15A1DE9C-27EF-4D0A-81DF-E0417196D753","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"7A117936-C128-47F8-84D2-1A3C5DBA4C50","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":16.00000000000023,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22499999999999112, 0}","curveMode":3,"curveTo":"{0.63749999999997953, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.49999999999999289, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77499999999999858}","curveMode":2,"curveTo":"{0, 0.22500000000000142}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.73124999999999241, 1}","curveMode":3,"curveTo":"{0.22499999999999112, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.49999999999999289, 1}"},{"_class":"curvePoint","do_objectID":"261A7BB7-7918-43C1-8D27-FC39440F388B","cornerRadius":0,"curveFrom":"{0.98124999999998885, 0.625}","curveMode":4,"curveTo":"{0.92499999999998406, 0.83749999999999858}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.99999999999998579, 0.625}"},{"_class":"curvePoint","do_objectID":"27222968-0B55-472C-AE54-1407E1911126","cornerRadius":0,"curveFrom":"{0.80000000000000004, 0.76874999999999716}","curveMode":4,"curveTo":"{0.84999999999999643, 0.625}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.87499999999998757, 0.625}"},{"_class":"curvePoint","do_objectID":"DF22ECE7-5C80-41A9-A9E9-9B1D5C1C21CE","cornerRadius":0,"curveFrom":"{0.29375000000001289, 0.875}","curveMode":3,"curveTo":"{0.66249999999999909, 0.875}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.49999999999999289, 0.875}"},{"_class":"curvePoint","do_objectID":"53943DFA-AA6A-4950-9E9C-8633972AC481","cornerRadius":0,"curveFrom":"{0.12500000000001243, 0.29375000000000284}","curveMode":2,"curveTo":"{0.12500000000001243, 0.70624999999999716}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.12499999999999822, 0.5}"},{"_class":"curvePoint","do_objectID":"8B7B0347-6EC3-471A-8AB5-CD06B2BB0EFE","cornerRadius":0,"curveFrom":"{0.60625000000000839, 0.125}","curveMode":3,"curveTo":"{0.29375000000001289, 0.125}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.49999999999999289, 0.125}"},{"_class":"curvePoint","do_objectID":"1D91563B-F479-4C72-BAB5-46F19D15D658","cornerRadius":0,"curveFrom":"{0.76250000000000617, 0.23749999999999716}","curveMode":4,"curveTo":"{0.69374999999999865, 0.16875000000000284}","hasCurveFrom":false,"hasCurveTo":true,"point":"{0.74999999999998934, 0.25}"},{"_class":"curvePoint","do_objectID":"54ED3EEF-9F06-41E9-AF50-47EC812276C7","cornerRadius":0,"curveFrom":"{0.56250000000000622, 0.4375}","curveMode":1,"curveTo":"{0.56250000000000622, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.56249999999999201, 0.4375}"},{"_class":"curvePoint","do_objectID":"8438DED0-BA59-4644-A2E3-FA34C7C5033A","cornerRadius":0,"curveFrom":"{1, 0.4375}","curveMode":1,"curveTo":"{1, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.99999999999998579, 0.4375}"},{"_class":"curvePoint","do_objectID":"FB4E35A0-C58C-465B-BBB4-FF18EC71CC06","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.99999999999998579, 0}"},{"_class":"curvePoint","do_objectID":"EFBA6B62-8E3C-4B84-B90E-BCFE1E58D208","cornerRadius":0,"curveFrom":"{0.76249999999997775, 0.0625}","curveMode":1,"curveTo":"{0.84999999999998221, 0.14999999999999858}","hasCurveFrom":true,"hasCurveTo":false,"point":"{0.87499999999998757, 0.125}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"C6E3F5B3-C968-48FF-9085-D3562D0EAA93"},{"_class":"symbolMaster","do_objectID":"76C8A3F1-FA9D-4553-9035-374EEF18CBF2","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"3BBD04CE-B0B0-48A6-BCC3-E8C8934CB89B","constrainProportions":false,"height":24,"width":24,"x":3782,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"Material\/Icons black\/arrow back","nameIsFixed":true,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","endDecorationType":0,"miterLimit":10,"startDecorationType":0},"hasClickThrough":true,"layers":[{"_class":"shapeGroup","do_objectID":"469B41DE-EE2A-427D-BEA8-B1DA6D113C8D","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","do_objectID":"DFD20384-5EF2-48B5-BEB4-74796F3663B7","constrainProportions":false,"height":16,"width":15.99999999999994,"x":4,"y":4},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Shape","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"style":{"_class":"style","do_objectID":"B29063E0-AB00-400D-A11C-8E5670377D84","endDecorationType":0,"fills":[{"_class":"fill","isEnabled":true,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0},"fillType":0,"noiseIndex":0,"noiseIntensity":0,"patternFillType":0,"patternTileScale":1}],"miterLimit":10,"sharedObjectID":"DC56376C-8162-4E24-BB3C-91C5B43AD324","startDecorationType":0},"hasClickThrough":false,"layers":[{"_class":"shapePath","do_objectID":"0EEF4BF2-0C9B-44A6-AB3B-C0421BA31C82","exportOptions":{"_class":"exportOptions","exportFormats":[],"includedLayerIds":[],"layerOptions":0,"shouldTrim":false},"frame":{"_class":"rect","constrainProportions":false,"height":16,"width":15.99999999999994,"x":0,"y":0},"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Path","nameIsFixed":false,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"booleanOperation":-1,"edited":true,"path":{"_class":"path","isClosed":true,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.4375}","curveMode":1,"curveTo":"{1, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.4375}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.23749999999999799, 0.4375}","curveMode":1,"curveTo":"{0.23749999999999799, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.23749999999999799, 0.4375}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.58750000000000069, 0.087499999999998579}","curveMode":1,"curveTo":"{0.58750000000000069, 0.087499999999998579}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.58750000000000069, 0.087499999999998579}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.50000000000000178, 0}","curveMode":1,"curveTo":"{0.50000000000000178, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.50000000000000178, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.5}","curveMode":1,"curveTo":"{0, 0.5}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.50000000000000178, 1}","curveMode":1,"curveTo":"{0.50000000000000178, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.50000000000000178, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.58750000000000069, 0.91250000000000142}","curveMode":1,"curveTo":"{0.58750000000000069, 0.91250000000000142}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.58750000000000069, 0.91250000000000142}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.23749999999999799, 0.5625}","curveMode":1,"curveTo":"{0.23749999999999799, 0.5625}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.23749999999999799, 0.5625}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.5625}","curveMode":1,"curveTo":"{1, 0.5625}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.5625}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.4375}","curveMode":1,"curveTo":"{1, 0.4375}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0.4375}"}]}}],"clippingMaskMode":0,"hasClippingMask":false,"windingRule":1}],"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"hasBackgroundColor":false,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":false,"symbolID":"B18F04FF-1BB0-41C0-939B-7EB372AF033F"}],"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeInCloudUpload":true,"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]}}
================================================
FILE: design-files/Moderator-StickerSheet-20161117-DAS/pages/226D6615-C16F-4B84-92E0-291B2F1B15C4.json
================================================
[File too large to display: 13.5 MB]
================================================
FILE: design-files/Moderator-StickerSheet-20161117-DAS/user.json
================================================
{"FDD7F69B-2AF0-4FAE-9B51-45D7040E8FCB":{"pageListHeight":1016,"exportableLayerSelection":["FAC6D00E-86E5-45BC-B761-CA9C39DD5233","DC571BE3-F1C5-4526-BA2A-9E4AB741EFF0","5798BAD5-8ED5-409A-87F4-567E0A63E777","66A8E8B0-AB0E-4B94-8E31-2C0F0E16F20A","0E479248-891F-42FD-8327-648B15C87E04","57C3F8E0-276C-4ACB-BDB3-8193E9EA7742","5B4982B7-8726-418E-9D14-2AEDA65B8DFF","6A87051D-B660-41EC-9BDB-488D63F0C56D","0ED4F5B1-85B9-4C2C-A3C9-B98E2D4980DA","83FDAE46-C90F-4972-A27A-53E25B1D2C95","DE991B07-B57F-4889-9D6E-D26F914CE247","759BEA3D-2EC8-4B10-BEA4-C945D63E9908","76B56D15-9C7B-41DD-B915-969F64DF243A","A4960E90-D518-4C70-ABD3-CCE07FB682AC","CD81B3A5-D489-484E-AC6B-000D7F3788D6","1A57493A-78A1-47A5-9CD8-E94EC457B557","6E06B227-EDA2-4053-89B7-92CD8D6D1A35","E2CB16E3-20B5-4210-A90E-E115BC41B9E9","AB3EA644-5829-4734-BAA8-A65E5A65C9AC","B36E8C3C-2368-48AF-9F48-BF0C929339B7","9C115BA1-01FD-4EB5-A0ED-3F302E44863B","933CC81D-2C9B-452A-AC5B-6405D4C607C4","B26015EB-FB4C-4310-A22D-DF70FE99CB1D","E18798F8-EB42-472D-8D37-4061B912D75F","971BFAFC-6721-4AD8-848B-439035F684E9","E580DDC7-059E-4509-A8C2-AA5DD98B9EFF","410F9419-53B5-49B2-8F33-917D0C005E78","D6681D07-6D98-45CC-BABB-0E966D101F3F","1A9E010D-ED70-4FEE-A2B7-AE50B4B6495D","C52C86AD-DFBD-47DB-918E-70E1E839263F","38A0707D-E950-4E99-902E-02B985A31968","131D9DA7-5413-48C7-8852-D77E2C92E339","08674E09-346B-4798-A00F-82D253CBA7A6","CF85FC69-3CEE-4AEA-8F1D-0A4D0F2447B7","CED58EDB-021E-42C9-A52E-A296F29B0E9B","C8F2A3CE-3FFE-4FCD-B841-BEFA971219A3","50080C0C-942A-4186-B601-FD70D4EB756D","9A65DFE6-0C2D-4B5E-BC48-4B30B0C39EBF","AF964B9C-3384-4A3E-B398-CDB2E3659409","4758FACA-A200-4913-8793-0D803A62EA75","711392F8-7726-4EF4-85C9-8E07F436E413","7E5FCE1E-3DC5-4048-9283-B61D8BE599B7","19986F6A-1B19-403F-B544-AF7D046D1200","6ED93965-5641-4CFF-BBCB-08B4FCAB98AC","A39A8B9A-83A0-4AF5-830A-902AEB658BE3","533F043C-236F-45E2-B277-6B683B7AB111","66256BA5-F050-482B-BFE3-0BF96B9B25E1","550873D6-3F8B-4EF2-9E4C-8C98B3405FB7","941D41CD-30D7-431B-B632-A9DD3FB56BFB","A3DC2D33-661C-4F1C-8E76-299C4C6683E0","DB4F3B63-3DDC-4A63-8B60-D4D5A00859BF"],"cloudShare":null},"1B67083D-3430-4865-A36A-6C687A1EEB45":{"scrollOrigin":"{-12647, 613}","zoomValue":4},"226D6615-C16F-4B84-92E0-291B2F1B15C4":{"scrollOrigin":"{9851, 2885}","zoomValue":1}}
================================================
FILE: docs/auth.md
================================================
# OS Moderator Authentication
OS Moderator leverages a Google OAuth 2 authentication flow to trade on our server for JWT tokens. The following outlines how it all works.
## Google OAuth Flow
Google OAuth (version 2) is used to actually authenticate users. [Passport](https://github.com/jaredhanson/passport) and [passport-google-oauth2](https://github.com/jaredhanson/passport-google-oauth2) are used on the backend and, upon receiving and verifying the access token:
1. A unique `User` model instance based on the email address, is created or retrieved
2. A unique `UserSocialAuth` model instance for the user / provider / provider id is created or retrieved
3. A JWT token is created and the user is redirected to the homepage with it in the query string, like: `/?token=(token string)`
## Protecting Resources
To make an endpoint require a valid JWT token, pass in Passport authentication middleware like so:
```js
router.get(
'/some/protected/path',
passport.authenticate('jwt', {session: false}),
(request, response) => {
response.send('Some private stuff!');
});
```
The JWT provider fetches and verifies the user encoded in the token with every request (see the `verify` function in `server/domain/auth/providers/jwt.ts`) and Passport makes a `user` object (a `User` Sequelize model instance) on the `request` object.
## Logging In
To log in, navigate to `/auth/login/google` and it will kick off the OAuth process with Google. You can optionally pass a `redirect` query string parameter of an encoded URL (e.g `encodeURIComponent('http://radsite.com')`) and it will be redirected to on successful login with a `token` query string parameter tacked on to it.
## Accessing Protected Resources
To use your JWT token to access protected resources, you must pass it into the `Authorization` HTTP header like so: `Authorization: JWT (token string)`.
## Token Expiration
Tokens expire for human users (as opposed to users whose `group` is set to `service`, which do not expire) after the number of minutes set in the configuration value `token_expiration_minutes` (`server/config/index.js`). The expiration is calculated server-side based on the `iat` (issued at timestamp) embedded in the token.
## Removing/Deactivating users
To remove/deactivate users, there is a simple `isActive` boolean field, which you can set to `false`/`0`. As long as the user record remains in the `users` table, the user should not be able to reauthenticate.
================================================
FILE: docs/comment_flow.md
================================================
# OSMOD Comment State Flow
This document describes the path of a comment through the OSMOD system.
1. The integration module (e.g., the YouTube module) pulls categories, articles and comments from the target system.
2. For each comment, we create a task to send the comment out to assistants for processing.
3. The task queue creates a `CommentScoreRequest` and submits [the request](osmod_assistant_protocol.md) to all users in the `service` group that have a configured `endpoint` path.
4. Each assistant `POST`s back to the `callback` URL they were given, which contains the specific `CommentScoreRequest` id from the original request.
5. Once all assistants have either called back, or timed out, a task is created to run automated `ModerationRule`s.
6. The rule task is handled by running all `ModerationRule`s against a single comment. If the rule results in a resolution (approve or reject), the comment is considered resolved.
7. If a rule did not resolve a comment, it is made available to the OSMOD frontend.
8. The OSMOD frontend can approve or reject comments either singularly or in bulk. The OSMOD frontend can also update the disposition of previously resolved comments.
9. For each resolved comment, the integration module updates the target system to implement the comment's final disposition.
10. Party 🎉🎉🎉
================================================
FILE: docs/modeling.md
================================================
# The Data Model for Osmod
## Connecting to remote SQL Google Cloud database
From [a cloud shell in your project](https://cloud.google.com/shell/docs/), you can run:
`gcloud beta sql connect --user=`
To connect to your SQL instance. You'll need to create the username from the
[google cloud SQL config interface](https://pantheon.corp.google.com/sql/instances/osmod-development-branch/users).
## The SQL Data Model
[The SQL table construction file](https://github.com/conversationai/conversationai-moderator/blob/master/seed/initial-database.sql)
Default database name: `os_moderator`
The tables:
Object | Tables_in_os_moderator | Description
-----------------------|---------------------------|-------------
- | SequelizeMeta | List of migrations that have been applied
User | users | OSMod Administrators, moderators, and other user-like entities
Category | categories | Categories from the moderated system (e.g., corresponds to channels for Youtube)
Article | articles | Articles from the moderated system (e.g., videos for YouTube)
Comment | comments | Comments from the moderated system
CommentFlag | comment_flag |
CommentScoreRequest | comment_score_requests |
CommentScore | comment_scores |
CommentSize | comment_sizes |
CommentSummaryScore | comment_summary_scores |
CommentTopScore | comment_top_scores |
Decision | decisions |
ModerationRule | moderation_rules |
Preselect | preselects |
TaggingSensitivity | tagging_sensitivities |
Tag | tags |
ModeratorAssignment | moderator_assignments | (Join table) Moderators assigned to moderate comments for the given article
UserCategoryAssignment | user_category_assignments | (Join table) Moderators assigned to moderate comments for the given category
CSRF | csrfs |
UserSocialAuth | user_social_auths |
### User
Users are users of Osmod. This is the moderation team, and people who admin the
Osmod system using the UI.
- id (int) (required)
- group (enum: general, admin, service, youtube) (required)
- email (string) (required for all but users in the "service" group)
- name (string) (required)
- isActive (tinyint) (required)
- avatarURL (string)
- extra (json)
*Indexes*:
- group
- isActive
- email (unique)
Service users serve 2 purposes:
- They are used to authenticate non-humans that want to access to the system. Use the [get-token](../README.md#the-osmod-cli) CLI command to create a suitable JWT token for this purpose.
- They are used to store the configuration for a moderator endpoint.
For the latter, the required configuration is stored in the extra field in a JSON blob that looks like
```json
{
serviceType: 'moderator',
endpointType: 'perspective-api',
endpoint: ,
extra:
}
```
### UserSocialAuth
This table holds information about the passport authentication configuration for
users in the `Users` table. It is used to allow users to login with social
networks, e.g. using their google account.
- id (int) (required)
- userId (foreign key: User) (required)
- socialId (string) (required) (provider user id)
- provider (string) (required) (name of auth provider, e.g. "google")
- extra (json)
*Indexes*:
- userId + provider (unique) (don't let the same user use the same provider multiple times)
- socialId + provider (unique) (don't allow the same provider user to authenticate on multiple accounts)
### Category
Represents a higher level collection of articles. Categories correspond to e.g., site sections, YouTube channels, or Reddit subreddits.
Moderation Rules are configured at the category level and apply to all articles in the category. Moderators can be assigned at this level.
- id (number) (required)
- sourceId (string) (optional) Original id from target system
- ownerId (foreign key: User) (optional) Service user that created this article.
- label (string) (required)
- isActive (boolean) (required, default true) Whether this category is being actively managed.
- count (int) Number of comments in this category
- unprocessedCount (int) Denormalize SUM of articles' unprocessedCount
- unmoderatedCount (int) Denormalize SUM of articles' unmoderatedCount
- moderatedCount (int) Denormalize SUM of articles' moderatedCount
- highlightedCount (int) Denormalize SUM of articles' highlightedCount
- approvedCount (int) Denormalize SUM of articles' approvedCount
- rejectedCount (int) Denormalize SUM of articles' rejectedCount
- deferredCount (int) Denormalize SUM of articles' deferredCount
- flaggedCount (int) Denormalize SUM of articles' flaggedCount
- batchedCount (int) Denormalize SUM of articles' batchedCount
- extra (json)
### Article
This table holds the articles that can be commented on.
- id (bigint) (required)
- sourceId (string) (required)
- ownerId (foreign key: User) (optional) Service user that created this article.
- sourceCreatedAt (Created ISO 8601 timestamp from target system)
- categoryId (foreign key: Category) (optional)
- title (string) (required)
- text (string) (required)
- url (string) (required)
- isAutoModerated (tinyint) (required) (Indicates whether the article is subject to automated moderation rules)
- isCommentingEnabled (boolean) (Indicates whether commenting is enabled. This field should be automatically synchronised with the host platform to enable/disable commenting.
- count (int) Number of comments in this article
- unprocessedCount (int) (Denormalized count of unprocessed comments (isScored = false))
- unmoderatedCount (int) (Denormalized count of unmoderated comments (isScored = true AND isModerated = false))
- moderatedCount (int) (Denormalized count of moderated comments (isScored = true AND isModerated = true))
- highlightedCount (int) (Denormalize COUNT of comments with highlightedCount > 0)
- approvedCount (int) (Denormalize COUNT of comments with approvedCount > 0)
- rejectedCount (int) (Denormalize COUNT of comments with rejectedCount > 0)
- deferredCount (int) (Denormalize COUNT of comments with deferredCount > 0)
- flaggedCount (int) (Denormalize COUNT of comments with flaggedCount > 0)
- batchedCount (int) (Denormalize COUNT of comments with batchedCount > 0)
- createdAt (datetime)
- modifiedAt (datetime)
- lastModeratedAt (datetime) Time when a moderation action was last done.
- extra (json)
*Indexes*:
- sourceId (unique)
- categoryId
### ModeratorAssignment
This tables holds which users are assigned to which articles.
- id (int) (required)
- user (foreign key: User) (required)
- article (foreign key: Article) (required)
*Indexes*:
- user + article (unique)
- user
### Comment
This table holds the comments, and the state of the comments.
- id (bigint) (required)
- sourceId (string) (required) (Original id from target system)
- ownerId (foreign key: User) (optional) Service user that uploaded this comment.
- replyToSourceId (string) (optional foreign key: self.sourceId)
- replyId (foreign key: Comment) (id of comment this is a reply to)
- authorSourceId (string) (required) (id of author on the target system)
- article (foreign key: Article) (required)
- author (json) (required)
- text (long text) (required)
- isScored (tinyint) (required)
- isModerated (tinyint)
- isAccepted (tinyint)
- isDeferred (tinyint)
- isHighlighted (tinyint)
- isBatchResolved (tinyint)
- isAutoResolved (tinyint) (Indicates if the comment was auto-accepted/rejected based on a rule(s))
- sourceCreatedAt (datetime) (required) (time comment created on target system)
- sentForScoring (datetime)
- sentBackToPublisher (datetime)
- extra (json)
*Indexes*
- sourceId (unique)
- isAccepted
- isDeferred
- isHighlighted
- isBatchResolved
- isAutoResolved
- sentForScoring
### CommentSize
- commentId (int) (required)
- width (int) (required)
- height (int) (required)
*Indexes*:
- commentId, width
### UserCategoryAssignment
- userId (int) (required)
- categoryId (int) (required)
*Indexes*:
- userId
- categoryId
#### Notes
- Used for hasAndBelongsToMany for category assignments on users.
#### States
- unscored: sentForScoring == null
- scored: sentForScoring != null && isScored == 1 (set as such when all related `CommentScoreRequest`s `doneAt` fields are set)
- accepted: isAccepted == 1
- rejected: isAccepted == 0
- deferred: isAccepted == null && isDeferred == 1
- highlighted: isAccepted == 1 && isHighlighted == 1
### CommentScoreRequest
- id (bigint)
- comment (foreign key: Comment) (required)
- userId (foreign key: User) (required)
- sentAt (datetime) (required)
- doneAt (datetime)
### CommentScore
- id (bigint)
- commentId (foreign key: Comment)
- sourceType (enum: User, Moderator, Machine) (required)
- sourceId (string) (optional identifier so that scores can be retracted, like for publisher recommendations)
- commentScoreRequestId (foreign key: CommentScoreRequest) (set for "machine" sources)
- score (float) (required) (0 - 1) (these get set to 1 for non-machine sources)
- annotationStart (int)
- annotationEnd (int)
- confirmedUserId (int)
- isConfirmed (tinyint)
- extra (json)
- createdAt (datetime) (required)
- updatedAt (datetime)
- TagId (int) (foreign key: Tags)
*Indexes*:
- comment
### CommentTopScore
- commentId (foreign key: Comment)
- tagId (foreign key: Tag)
- commentScoreId (foreign key: CommentScore)
*Indexes*:
- commentId/tagId
### CommentSummaryScore
- commentId (foreign key: Comment)
- tagId (foreign key: Tag)
- score (float) (required)
- confirmedUserId (int)
- isConfirmed (bool)
*Indexes*:
- commentId/tagId
### CommentFlag
An attribute of the comment indicating the comment has been flagged for some reason on the target platform.
Currently, there is no means of setting this flag in OSMod, though we display counts of flagged comments.
TODO: Document how a flagged comment appears in the UI.
- id (bigint)
- commentId (foreign key: Comment)
- sourceId (string) (optional identifier so that scores can be retracted, like for publisher recommendations)
- extra (json)
- createdAt (datetime) (required)
- updatedAt (datetime)
*Indexes*:
- commentId
### Decision
- id(int) (required)
- commentId (foreign key: Comment) (require)
- userId (foreign key: User) (optional, if source === User)
- moderationRuleId (foreign key: ModerationRule) (optional, if source === Rule)
- status (enum: Accept, Reject, Defer) (required)
- source (enum: User, Rule) (required)
- sentBackToPublisher (datetime)
#### Notes
Represents a log of decisions made by OSMod.
### ModerationRule
- id (int) (required)
- tagId (foreign key: Tag) (required)
- categoryId (foreign key: Category)
- lowerThreshold (smallint) (required)
- upperThreshold (smallint) (required)
- action (enum: Approve, Reject, Defer, Highlight) (required)
- createdBy (foreign key: User)
*Indexes*:
- categoryId
### CommentReply
- commentId (int) (required)
- replyId (int) (required)
*Indexes*:
- commentId
- replyId
#### Notes
- Used for hasAndBelongsToMany for replies on a comment.
### Tag
- id (int) (required)
- key (string) (required) (raw key for tag, e.g. `ATTACK_ON_COMMENTER`)
- label (string) (required) (display name, e.g. `Attack of Commenter`)
- color (string) (required) (hex color, e.g. `#c0ff33`)
- description (string) (optional) (short description, e.g. `A verbale attack directed towards author`)
- isInBatchView (bool) (is the tag to be shown on the front-end)
- isTaggable (bool) (tags that would show up in reason to reject or moderateor selected tags, but not the tag selector for batch view)
*Indexes*:
- key (unique)
### Preselect
- id (int) (required)
- tagId (foreign key: Tag)
- categoryId (foreign key: Category)
- lowerThreshold (smallint) (required)
- upperThreshold (smallint) (required)
- createdBy (foreign key: User)
### TaggingSensitivity
- id (int) (required)
- tagId (foreign key: Tag)
- categoryId (foreign key: Category)
- lowerThreshold (smallint) (required)
- upperThreshold (smallint) (required)
- createdBy (foreign key: User)
## Mappings
### YouTube
For YouTube we use the following map:
* YouTube Channel -> OSMod Category
* YouTube Video -> OSMod Article
* YouTube Comment -> OSMod Comment
YouTube allows comments on the channel as well as the Video. To accommodate this, there is a special article that corresponds to the channel itself, labelled "Channel comments."
You can connect multiple YouTube accounts to a single OSMod instance. Each connection has an associated user of type "youtube" that stores the necessary authentication credentials.
We set the ownerID field of each category/article/comment fetched from YouTube to point to the YouTube user that is responsible for this entity.
================================================
FILE: docs/osmod_assistant_protocol.md
================================================
# OSMOD Assistant Protocol
This document describes the protocol for interacting with the "assistant", the
component that bridges between the OSMOD backend and the machine learning
models.
The OSMOD backend provides comments and articles to the assistant, which in
turn gives scores on each comment.
## Send a Comment for Scoring
To get scores for a comment, the OSMOD backend sends a `POST` to the assistant
endpoint `/api/score-comment` in the following format:
```javascript
{
"comment": {
"commentId": "123",
// UTF-8 text of the comment.
"plainText": "We are condemned to act out this sad, once unimaginable farce. Sad!",
// Optional HTML content.
"htmlText": "We are condemned to act out this sad, once unimaginable farce. Sad!",
"links": {
// OSMOD API endpoint for retrieving more data about the comment.
"self": "https://osmod-backend/api/rest/comments/123",
},
},
"article": {
"articleId": "456",
// UTF-8 text of the article.
"plainText": "The beauty of me is that I'm very rich.",
// Optional HTML content.
"htmlText": "The beauty of me is that I'm very rich.",
"links": {
// OSMOD API endpoint for retrieving more data about the comment.
"self": "https://osmod-backend/api/rest/articles/456",
},
},
// Single comment that this comment is responding to. Often null.
"inReplyToComment": {
"commentId": "789",
// UTF-8 text of the comment.
"plainText": "We are condemned to act out this sad, once unimaginable farce. Sad!",
// Optional HTML content.
"htmlText": "We are condemned to act out this sad, once unimaginable farce. Sad!",
"links": {
// OSMOD API endpoint for retrieving more data about the comment.
"self": "https://osmod-backend/api/rest/comments/789"
},
},
// Whether to include summary scores for the comment. Optional: by default,
// summary scores aren't included. See documentation of response object below.
"includeSummaryScores": true,
"links": {
// Full URL of backend endpoint that the assistant should post scores to.
// See next section.
"callback": "https://osmod-backend/api/assistant/comment-scores/123"
}
}
```
Once the assistant has scores for the comment, it will `POST` them to the
endpoint specified in `links.callback`. That endpoint is described next.
## Receive Comment Scores
The OSMOD backend will provide the endpoint
`/api/assistant/comment-scores/:id`. The comment ID is embedded as the last
parameter in the URL, which is constructed and sent as the `links.callback`
field in the scoring request described above.
The POST data from the assistant is in the following format:
```javascript
{
// A map from "attribute" to list of score objects. Each score object contains
// a score value for a span of the original comment text. There may be
// multiple score objects for each attribute that describe different text
// spans.
//
// The possible attribute string values are:
// ATTACK_ON_AUTHOR
// ATTACK_ON_COMMENTER
// ATTACK_ON_PUBLISHER
// INCOHERENT
// INFLAMMATORY
// LIKELY_TO_REJECT
// OBSCENE
// OFF_TOPIC
// SPAM
// UNSUBSTANTIAL
"scores": {
"ATTACK_ON_COMMENTER": [
{
// Number between 0 and 1, inclusive. Greater values mean higher
// confidence that the attribute applies to this span of text.
"score": 0.2,
// Integer describing the span of the original comment text that
// the score applies to. The values are in UTF-16 codepoints. "end" is
// exclusive.
// Example: for the text "Hi - I have the best words!", the begin/end
// pair of (0,2) describes the string "Hi", and the pair (5,26)
// describes the string "I have the best words".
"begin": 0,
"end": 62,
},
],
"INFLAMMATORY": [
{
"score": 0.4,
"begin": 0,
"end": 62,
},
{
"score": 0.7,
"begin": 63,
"end": 66,
},
],
},
// A map from "attribute" to a single overall score for the entire comment.
// The set of keys between `summaryScores` and `scores` should be the same
// (that is, an attribute with per-span scores should also have a summary
// score, and vice versa).
//
// `summaryScores` is returned if `includeSummaryScores` was true in the
// request.
"summaryScores": {
"ATTACK_ON_COMMENTER": 0.2,
"INFLAMMATORY": 0.45
},
// String describing problems encountered during scoring. The `scores` and
// `error` fields should be mutually exclusive.
"error": "Problem scoring text: connection to ML backend timed out.",
}
```
## Assistant connection details
The assistant can be reached at https://osmod-assistant.appspot.com/.
For testing/debugging, one can specify the `sync` field in the scoring request,
and the assistant will respond with the score result, as opposed to posting the
result to the `links.callback` endpoint.
Example:
```
$ curl -H 'Content-Type: application/json' --data '{"sync": true, "comment": {"plainText": "you big darn dummy!"} }' https://osmod-assistant.appspot.com/api/score-comment
{"scores":{"ATTACK_ON_COMMENTER":[{"score":0.66,"begin":0,"end":18}],"INFLAMMATORY":[{"score":0.792,"begin":0,"end":18}],"OBSCENITY":[{"score":0.2,"begin":8,"end":11}]}}
```
================================================
FILE: docs/osmod_services_api.md
================================================
# OSMOD Services API
The OSMOD Services API allows publishing and tagging operations to comments.
This documentation covers the services:
Comment Actions
> * /api/services/commentActions/approve
> * /api/services/commentActions/reject
> * /api/services/commentActions/defer
> * /api/services/commentActions/highlight
> * /api/services/commentActions/tag/:tagid
> * /api/services/commentActions/tagCommentSummaryScores/:tagid
Comment Score Actions
> * /api/services/commentActions/:commentid/scores
> * /api/services/commentActions/:commentid/scores/:commentscoreid/reset
> * /api/services/commentActions/:commentid/scores/:commentscoreid/confirm
> * /api/services/commentActions/:commentid/scores/:commentscoreid/reject
> * /api/services/commentActions/:commentid/scores/:commentscoreid
## Comment Actions
Comment Actions allow control over a comment to approve, reject, highlight, defer, and tag.
### approve
Approve all comment id(s)
A `POST` to `/api/services/commentActions/approve` with a body containing the ID(s) of the comment(s) to trigger the action upon.
Body Example using single commentId:
```javascript
{
"data" : [
{ commentId: '12', userId: '1' }
]
}
```
Or, for a bulk command using multiple commentId
```javascript
{
"data" : [
{ commentId: '12', userId: '1' },
{ commentId: '13', userId: '1' },
{ commentId: '17', userId: '1' }
]
}
```
### reject
Reject all comment id(s)
A `POST` to `/api/services/commentActions/reject` with a body containing the ID(s) of the comment(s) to trigger the action upon.
Body Example using single commentId:
```javascript
{
"data" : [
{ commentId: '12', userId: '1' }
]
}
```
Or, for a bulk command using multiple commentId
```javascript
{
"data" : [
{ commentId: '12', userId: '1' },
{ commentId: '13', userId: '1' },
{ commentId: '17', userId: '1' }
]
}
```
### defer
Defer all comment id(s)
A `POST` to `/api/services/commentActions/defer` with a body containing the ID(s) of the comment(s) to trigger the action upon.
Body Example using single commentId:
```javascript
{
"data" : [
{ commentId: '12', userId: '1' }
]
}
```
Or, for a bulk command using multiple commentId
```javascript
{
"data" : [
{ commentId: '12', userId: '1' },
{ commentId: '13', userId: '1' },
{ commentId: '17', userId: '1' }
]
}
```
### highlight
Highlight all comment id(s)
A `POST` to `/api/services/commentActions/highlight` with a body containing the ID(s) of the comment(s) to trigger the action upon.
Body Example using single commentId:
```javascript
{
"data" : [
{ commentId: '12', userId: '1' }
]
}
```
Or, for a bulk command using multiple commentId
```javascript
{
"data" : [
{ commentId: '12', userId: '1' },
{ commentId: '13', userId: '1' },
{ commentId: '17', userId: '1' }
]
}
```
### tag
Tag all comment id(s) with the tag ID provided.
A `POST` to `/api/services/commentActions/tag/:tagid` with a body containing the ID(s) of the comment(s) to trigger the action upon.
Body Example using single commentId:
```javascript
{
"data" : [
12
]
}
```
Or, for a bulk command using multiple commentId
```javascript
{
"data" : [
12,13,17
]
}
```
### tagCommentSummaryScores
Tag all comment id(s) comment summary score with the tag ID provided.
A `POST` to `/api/services/commentActions/tagCommentSummaryScores/:tagid` with a body containing the ID(s) of the comment(s) to trigger the action upon.
Body Example using single commentId:
```javascript
{
"data" : [
12
]
}
```
Or, for a bulk command using multiple commentId
```javascript
{
"data" : [
12,13,17
]
}
```
## Comment Score Actions
Comment Score Actions allow control over the internal content of a comment. This allows specific words or phrases to be tagged and controlled. The comment detail actions allow control over tag adding, removal, rejecting and confirming.
### add
Add a tag to a set of content within a single comment.
A `POST` to `/api/services/commentActions/:commentid/scores` with a body containing the object:
```javascript
{
"data" : {
// The tag id of the tag to be added to this selection
"tagId": "1",
// The start position of the character in the string of the comment text.
"annotationStart": 130,
// The end position of the character in the string of the comment text.
"annotationEnd": 145
}
}
```
### remove
Remove a tag from the comment.
A `DELETE` to `/api/services/commentActions/:commentid/scores/:commentscoreid`
### confirm
Confirming a tag that a previous user (or machine) has added to a particular set of content within the comment.
A `POST` to `/api/services/commentActions/:commentid/scores/:commentscoreid/confirm` with no body
### reject
Rejecting a tag that a previous user (or machine) has added to a particular set of content within the comment.
A `POST` to `/api/services/commentActions/:commentid/scores/:commentscoreid/reject` with no body
================================================
FILE: docs/osmod_task_api.md
================================================
#OSMODTaskAPI
The OSMOD Task API exposes the worker tasks on HTTP endpoints to support
`push`-style message consumption.
This documentation covers the following tasks:
```text
processMachineScore
heartbeat
processTagAddition
processTagRevocation
sendCommentForScoring
deferComments
highlightComments
tagComments
tagCommentSummaryScores
acceptComments
rejectComments
resetTag
resetComments
confirmTag
confirmCommentSummaryScore
rejectCommentSummaryScore
rejectTag
addTag
removeTag
```
## Payload Formats
All payloads should be wrapped in a `data` object of the form...
```json
{
"data": {...}
}
```
```json
{
"data": [...]
}
```
Refer to [backend-queue/tasks](https://github.com/Jigsaw-Code/moderator/tree/dev/packages/backend-queue/src/tasks) for the task schemas.
Field names correspond to key-value in the JSON objects and the types are the expected payload type formats.
For example, take a `processMachineScore` task with the following data interface.
```typescript
export interface IProcessMachineScoreData {
commentId: number;
userId: number;
scoreData: IScoreData;
runImmediately?: boolean;
}
```
Its payload would look like the following JSON body.
```json
{
"data": {
"commentId": "1",
"userId": "1",
"scoreData": {
"scores": [{"score": 0.8, "begin": 1, "end": 53}],
"summaryScores": {
"OBSCENE": 0.9,
"INFLAMMATORY": 0.7
}
},
"runImmediately": true
}
}
```
================================================
FILE: docs/sql_queries.sql
================================================
-- articles with counts of real comments, and the table's counts cache for
-- and unmoderated and moderated.
CREATE VIEW articles_with_counts AS
SELECT a.id, a.categoryId, a.unmoderatedCount, a.moderatedCount, m.commentCount, a.title
FROM
((SELECT articleId, COUNT(*) as commentCount
FROM comments GROUP BY articleId) AS m
INNER JOIN articles AS a
ON a.id = m.articleId);
-- Categories with the counts
CREATE VIEW category_ids_with_counts AS
(SELECT categoryId, COUNT(*) as articleCount,
SUM(commentCount) as commentCount,
SUM(unmoderatedCount) as unmoderatedCount,
SUM(moderatedCount) as moderatedCount
FROM articles_with_counts
GROUP BY categoryId);
-- Named categories with counts
CREATE VIEW categories_with_counts AS
(SELECT g.id, g.label, g.unmoderatedCount,
g.moderatedCount, c.articleCount, c.commentCount
FROM (category_ids_with_counts AS c
INNER JOIN categories AS g
ON c.categoryId = g.id));
-- See counter for some articles.
SELECT * FROM articles_with_counts LIMIT 10;
-- See the counts for categories
SELECT * FROM categories_with_counts LIMIT 10;
-- Reset Article Counts
UPDATE articles AS a
INNER JOIN articles_with_counts AS c
ON a.id = c.id
SET
a.moderatedCount = 0,
a.unmoderatedCount = c.commentCount;
-- Update Category Counts
UPDATE categories AS g
INNER JOIN categories_with_counts AS c
ON g.id = c.id
SET
g.moderatedCount = 0,
g.unmoderatedCount = c.commentCount;
-- Reset comment's moderation state
UPDATE comments SET
isModerated = false,
sentBackToPublisher = NULL,
isAccepted = false,
isDeferred = false,
isHighlighted = false,
isBatchResolved = false,
isAutoResolved = false;
-- Remove the decisions made
DELETE FROM decisions;
-- Reset article counts
UPDATE articles SET
moderatedCount = 0,
highlightedCount = 0,
approvedCount = 0,
rejectedCount = 0,
deferedCount = 0,
batchedCount = 0;
-- Reset category counts
UPDATE categories SET
moderatedCount = 0,
highlightedCount = 0,
approvedCount = 0,
rejectedCount = 0,
deferedCount = 0,
batchedCount = 0;
================================================
FILE: docs/worker.md
================================================
# OS Moderator Worker/Task Queue
We have a worker/task queue using [Kue](https://github.com/Automattic/kue) and [Kue Scheduler](https://github.com/lykmapipo/kue-scheduler) for repeating tasks, which uses [Redis](http://redis.io/) as its backend.
## Running the worker
### In Development
#### With Docker
You can run all services from the root of the project like so:
```bash
docker-compose up
```
This will run the client, server, MySQL, Redis, and the worker.
#### Running Ad-Hoc
You'll need to make [Redis](http://redis.io/) is installed, which you can do with [Homebrew](http://brew.sh/):
```bash
brew install redis
```
And you can run it like so:
```bash
redis-server /usr/local/etc/redis.conf
```
There's a watch command to compile Typescript and run the worker:
```bash
cd server
npm run watch:worker
```
### In Real Life
For production the worker should be run like so:
```bash
cd server
npm run compile
node dist/worker/index.js
```
## Tasks
### Adding New Tasks
All tasks are conventionally organized into their own files under `server/worker/tasks/` and loaded passively by `server/worker/tasks/index.ts`, which is loaded by the worker entrypoint file at `server/worker/index.ts`.
### Queuing Tasks
When queuing a task, all that's happening is that meta information around the task is being logged to Redis, to be picked up by a running worker. This means that you can queue tasks as long as Redis is running, but they will not actually be executed unless a worker is also running to pick them up. Keep this in mind if you're queuing up lots of tasks without a worker running, as when you spin it up it will start processing them immediately.
To avoid this, particularly for local development, sometimes it's best to flush Redis (WARNING: This removes _everything_ from Redis):
```bash
# Spin up a Redis REPL and enter the 'FLUSHALL' command to delete everything
redis-cli
127.0.0.1:6379> FLUSHALL
```
To programmatically queue up a task, do the following:
```js
import { queue } from './worker/queue';
queue
.create('nameOfTask', {someArgument1: 5, someArgument: 'Hello'})
.save();
```
### Repeating/Scheduled Tasks
[kue-scheduler](https://github.com/lykmapipo/kue-scheduler) is in place to support repeating/scheduled tasks. To create a repeating task, you must define one, then in the main worker entry point, `worker/index.ts`, inside of the conditional checking whether to run scheduled tasks or not (`if (config.get('worker.run_scheduled_tasks')) { ... }`) you'll add your schedule and it should look something like this:
```js
const repeatingJob = queue
// This is standard a Kue function to create a run of a task
.create('nameOfYourTask')
// This makes your repeating task unique, so that the runs of
// it won't overlap
// Uses: https://github.com/lykmapipo/kue-unique
.unique(true)
// This makes sure your job is removed on completion and that it
// will repeat. Without this, the job run information will stay in
// Redis and the `.unique` call will make it so it doesn't run again
.removeOnComplete(true)
// Time to live in milliseconds. This is a standard Kue function and
// is a good idea so your job runs don't hang if there are issues with it
.ttl(1000 * 60);
// This schedules your task, the time syntax accepts a cron-ish syntax:
// https://github.com/ncb000gt/node-cron
// ... as well as human readable intervals:
// https://github.com/rschmukler/human-interval
queue.every('6 hours', repeatingJob);
```
#### Issues with changing intervals
Kue scheduer is pretty finnicky and if you change the time interval, it seems to have issues picking it up, as it seems to store bits of data about the repeating data in various spots in the Redis DB that it doesn't resolve intuitively. You can fix this by running a FLUSHALL on Redis in your local environment if you don't care about deleting everything, but this should probably not happen in production. You can rename the task (maybe even the unique key...) and that would solve it.
================================================
FILE: docs/youtube_integration.md
================================================
# OS Moderator YouTube integration
## Synchronisation of Channels and Videos
In the OSMod UI, Channels are mapped to categories/sections, Videos are mapped to articles.
When syncing with YouTube, we fetch all channels that the YouTube account can access, but we only
synchronise against videos that we already know about. In particular, if a video has no comments,
then it will not appear.
Channel/Video synchronisation is kicked periodically (currently once a day). Though the UI provides a mechanism
for doing an immediate sync. Synchronisation occurs only while OSMod is active, i.e., when there is at least
one moderator actively using the tool.
Channel and Video data is fetched using the following APIS:
### Channel data:
API: google.youtube('v3').channels.list
For each channel we access the snippet brandingSettings:
we are only interested in the id, snippet.titlename, and brandingSettings.channel.moderateComments fields.
If moderateComments is false, we assume that this channel is not being managed by OSModerator,
and take no further action.
We eventually plan on providing a mechanism for enabling moderateComments via the OSMOD UI.
### Video data
API: google.youtube('v3').videos.list
We fetch videos that we know about and that are being actively managed. A video is actively managed if its
channel is active and we've seen some comments for that video.
For each video, we store the id, title, description, channel ID and URL.
## Synchronisation of Comments
API: google.youtube('v3').commentThreads.list
For each active channel, we periodically poll the API for new comments to process.
We do this every few minutes. We narrow the search by only requesting
those comments that haven't yet been moderated (heldForReview).
For each comment and reply, we are interested in the following:
- id
- snippet.textDisplay
- snippet.publishedAt
- snippet.videoId
- snippet.authorDisplayName
- snippet.authorProfileImageUrl
- snippet.authorChannelId.value
### Backsync of comments
API: google.youtube('v3').comments.setModerationStatus
We request the snippet and replies.
Once we have decided what to do with a comment, we set its moderation status via the above API:
================================================
FILE: lerna.json
================================================
{
"version": "1.1.0"
}
================================================
FILE: package.json
================================================
{
"name": "osmod",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/conversationai/conversationai-moderator"
},
"engines": {
"node": ">=12.13.1",
"npm": ">=6.12.1"
},
"dependencies": {
"lerna": "7.1.1",
"lerna-audit": "1.3.3",
"tslib": "2.2.0",
"tslint": "6.1.3",
"tslint-react": "5.0.0",
"typescript": "4.2.4"
}
}
================================================
FILE: packages/README.md
================================================
# The Osmod Packages
There are several parts to Osmod:
* `backend-api`: the codebase of the API server for Osmod.
* Uses: `config`, `frontend-web`
* `config`: common project configure files.
* `frontend-web`: the web frontend for Osmod.
================================================
FILE: packages/backend-api/.sequelizerc
================================================
const path = require('path');
module.exports = {
'config': path.resolve('./dist/sequelize-config.js'),
'migrations-path': path.resolve('./src/migrations'),
'models-path': path.resolve('./dist/models')
};
================================================
FILE: packages/backend-api/LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright {2016} {Jigsaw}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: packages/backend-api/README.md
================================================
# Moderator Backend
The Moderator Backend is a Node.js/Express-backed service that provides several APIs for both the front end as well as interactions with external services.
## Table of Contents
* [Installation](#installation)
* [Scripts](#scripts)
* [Worker/Task Queue](docs/worker.md)
* [Modeling](docs/modeling.md)
* [Auth](docs/auth.md)
* [OS Mod](docs/osmod_rest_api.md)
* [Troubleshooting](troubleshooting)
# Installation
Adding docs on auth, adding table of contents to README
This section will get the project running with all of its setup and dependencies.
## Requirements
* OS X
* [Docker Engine 1.12+](https://docs.docker.com/engine/installation/)
* [Docker Compose 1.8+](https://docs.docker.com/compose/install/) (this gets installed with "Docker for Mac", but you can also install it piece-meal)
## Deployment
See [docs/deployment.md](docs/deployment.md).
## Testing
We use Mocha.js and Chai for testing. Tests will be run automatically on the continuous integration server automatically before deployment, so make sure you run tests locally before pushing anything.
To run the tests locally, simple run the following from the `server` directory on your VM:
```
npm test
```
## Linting
The easiest way to lint your work is to run the linting script! From `server` directory on your VM:
```
npm run lint
```
This will fire off all the linters and fail if any code doesn't pass muster. Note that we run the linter script during the build, so if you're code doesn't pass linting the build *will* fail. Loudly.
### TSLint
We use [TSLint](https://palantir.github.io/tslint/) for linting backend Typescript.
## Running the server in HTTPS mode
Create a test certificate via the following command
```bash
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
```
copy the resulting files to the directory `packages/backend-api/sslcert`.
================================================
FILE: packages/backend-api/bin/check_migrations.sh
================================================
#!/bin/bash
# Do a sequelize sync and dump the resulting schema:
export DATABASE_NAME=os_moderator_schema_test_sync
sudo mysql << EOF
DROP DATABASE IF EXISTS $DATABASE_NAME;
CREATE DATABASE $DATABASE_NAME;
GRANT ALL on $DATABASE_NAME.* to $DATABASE_USER;
EOF
bin/run_sequelize_sync.js
# Now do the same thing, but this time loading base database and running through
# the migrations
export DATABASE_NAME=os_moderator_schema_test_migrations
sudo mysql << EOF
DROP DATABASE IF EXISTS $DATABASE_NAME;
CREATE DATABASE $DATABASE_NAME;
GRANT ALL on $DATABASE_NAME.* to $DATABASE_USER;
EOF
sudo mysql $DATABASE_NAME < ../../seed/initial-database.sql
npx sequelize db:migrate
sudo mysql-schema-diff os_moderator_schema_test_migrations os_moderator_schema_test_sync
================================================
FILE: packages/backend-api/bin/make_migration.sh
================================================
#!/bin/bash
npx sequelize migration:create --name $1
================================================
FILE: packages/backend-api/bin/osmod-test.js
================================================
#!/usr/bin/env node
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const path = require('path');
const yargs = require('yargs');
const youtube = require(path.join(__dirname, '..', 'dist', 'commands', 'tests', 'youtube'));
const generate = require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'generate'));
const imprt = require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'import'));
yargs
.command(youtube)
.command(generate)
.command(imprt)
.demand(1)
.demandCommand(1, 'no command specified')
.usage('Usage: $0 [options]')
.help()
.onFinishCommand(() => {process.exit()})
.argv;
================================================
FILE: packages/backend-api/bin/osmod.js
================================================
#!/usr/bin/env node
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Entrypoint for the osmod commandline tool.
*
* Provides commandline access to various features, including
* - managing users
* - managing comments
*
* For a full list of available commands, run `osmod.js --help`
*/
'use strict';
const path = require('path');
const yargs = require('yargs');
yargs
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'users', 'create')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'users', 'get_token')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'rescore')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'send_to_scorer')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'calculate_text_size')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'recalculate_text_sizes')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'recalculate_top_scores')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'flag')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'comments', 'delete')))
.command(require(path.join(__dirname, '..', 'dist', 'commands', 'denormalize')))
.demand(1)
.usage('Usage: $0')
.help()
.argv;
================================================
FILE: packages/backend-api/bin/run_sequelize_sync.js
================================================
#!/usr/bin/env node
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const { sequelize } = require('../dist/sequelize-sync');
(async () => {
await sequelize.sync({ force: true });
await sequelize.close();
process.exit();
})();
================================================
FILE: packages/backend-api/bin/run_task
================================================
#!/usr/bin/env node
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
const { runTask } = require('../dist');
runTask(process.argv[2] || '');
================================================
FILE: packages/backend-api/data/alice.txt
================================================
Taken from project Gutenberg
http://www.gutenberg.org/ebooks/11
_THE "STORYLAND" SERIES_
ALICE'S ADVENTURES IN WONDERLAND
SAM'L GABRIEL SONS & COMPANY
NEW YORK
Copyright, 1916,
by SAM'L GABRIEL SONS & COMPANY
NEW YORK
ALICE'S ADVENTURES IN WONDERLAND
[Illustration]
I--DOWN THE RABBIT-HOLE
Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do. Once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, "and what is the use of a book," thought Alice, "without pictures or
conversations?"
So she was considering in her own mind (as well as she could, for the
day made her feel very sleepy and stupid), whether the pleasure of
making a daisy-chain would be worth the trouble of getting up and
picking the daisies, when suddenly a White Rabbit with pink eyes ran
close by her.
There was nothing so very remarkable in that, nor did Alice think it so
very much out of the way to hear the Rabbit say to itself, "Oh dear! Oh
dear! I shall be too late!" But when the Rabbit actually took a watch
out of its waistcoat-pocket and looked at it and then hurried on, Alice
started to her feet, for it flashed across her mind that she had never
before seen a rabbit with either a waistcoat-pocket, or a watch to take
out of it, and, burning with curiosity, she ran across the field after
it and was just in time to see it pop down a large rabbit-hole, under
the hedge. In another moment, down went Alice after it!
[Illustration]
The rabbit-hole went straight on like a tunnel for some way and then
dipped suddenly down, so suddenly that Alice had not a moment to think
about stopping herself before she found herself falling down what seemed
to be a very deep well.
Either the well was very deep, or she fell very slowly, for she had
plenty of time, as she went down, to look about her. First, she tried to
make out what she was coming to, but it was too dark to see anything;
then she looked at the sides of the well and noticed that they were
filled with cupboards and book-shelves; here and there she saw maps and
pictures hung upon pegs. She took down a jar from one of the shelves as
she passed. It was labeled "ORANGE MARMALADE," but, to her great
disappointment, it was empty; she did not like to drop the jar, so
managed to put it into one of the cupboards as she fell past it.
Down, down, down! Would the fall never come to an end? There was nothing
else to do, so Alice soon began talking to herself. "Dinah'll miss me
very much to-night, I should think!" (Dinah was the cat.) "I hope
they'll remember her saucer of milk at tea-time. Dinah, my dear, I wish
you were down here with me!" Alice felt that she was dozing off, when
suddenly, thump! thump! down she came upon a heap of sticks and dry
leaves, and the fall was over.
Alice was not a bit hurt, and she jumped up in a moment. She looked up,
but it was all dark overhead; before her was another long passage and
the White Rabbit was still in sight, hurrying down it. There was not a
moment to be lost. Away went Alice like the wind and was just in time to
hear it say, as it turned a corner, "Oh, my ears and whiskers, how late
it's getting!" She was close behind it when she turned the corner, but
the Rabbit was no longer to be seen.
She found herself in a long, low hall, which was lit up by a row of
lamps hanging from the roof. There were doors all 'round the hall, but
they were all locked; and when Alice had been all the way down one side
and up the other, trying every door, she walked sadly down the middle,
wondering how she was ever to get out again.
Suddenly she came upon a little table, all made of solid glass. There
was nothing on it but a tiny golden key, and Alice's first idea was that
this might belong to one of the doors of the hall; but, alas! either the
locks were too large, or the key was too small, but, at any rate, it
would not open any of them. However, on the second time 'round, she came
upon a low curtain she had not noticed before, and behind it was a
little door about fifteen inches high. She tried the little golden key
in the lock, and to her great delight, it fitted!
[Illustration]
Alice opened the door and found that it led into a small passage, not
much larger than a rat-hole; she knelt down and looked along the passage
into the loveliest garden you ever saw. How she longed to get out of
that dark hall and wander about among those beds of bright flowers and
those cool fountains, but she could not even get her head through the
doorway. "Oh," said Alice, "how I wish I could shut up like a telescope!
I think I could, if I only knew how to begin."
Alice went back to the table, half hoping she might find another key on
it, or at any rate, a book of rules for shutting people up like
telescopes. This time she found a little bottle on it ("which certainly
was not here before," said Alice), and tied 'round the neck of the
bottle was a paper label, with the words "DRINK ME" beautifully printed
on it in large letters.
"No, I'll look first," she said, "and see whether it's marked '_poison_'
or not," for she had never forgotten that, if you drink from a bottle
marked "poison," it is almost certain to disagree with you, sooner or
later. However, this bottle was _not_ marked "poison," so Alice ventured
to taste it, and, finding it very nice (it had a sort of mixed flavor of
cherry-tart, custard, pineapple, roast turkey, toffy and hot buttered
toast), she very soon finished it off.
* * * * *
"What a curious feeling!" said Alice. "I must be shutting up like a
telescope!"
And so it was indeed! She was now only ten inches high, and her face
brightened up at the thought that she was now the right size for going
through the little door into that lovely garden.
After awhile, finding that nothing more happened, she decided on going
into the garden at once; but, alas for poor Alice! When she got to the
door, she found she had forgotten the little golden key, and when she
went back to the table for it, she found she could not possibly reach
it: she could see it quite plainly through the glass and she tried her
best to climb up one of the legs of the table, but it was too slippery,
and when she had tired herself out with trying, the poor little thing
sat down and cried.
"Come, there's no use in crying like that!" said Alice to herself rather
sharply. "I advise you to leave off this minute!" She generally gave
herself very good advice (though she very seldom followed it), and
sometimes she scolded herself so severely as to bring tears into her
eyes.
Soon her eye fell on a little glass box that was lying under the table:
she opened it and found in it a very small cake, on which the words "EAT
ME" were beautifully marked in currants. "Well, I'll eat it," said
Alice, "and if it makes me grow larger, I can reach the key; and if it
makes me grow smaller, I can creep under the door: so either way I'll
get into the garden, and I don't care which happens!"
She ate a little bit and said anxiously to herself, "Which way? Which
way?" holding her hand on the top of her head to feel which way she was
growing; and she was quite surprised to find that she remained the same
size. So she set to work and very soon finished off the cake.
[Illustration]
II--THE POOL OF TEARS
"Curiouser and curiouser!" cried Alice (she was so much surprised that
for the moment she quite forgot how to speak good English). "Now I'm
opening out like the largest telescope that ever was! Good-by, feet! Oh,
my poor little feet, I wonder who will put on your shoes and stockings
for you now, dears? I shall be a great deal too far off to trouble
myself about you."
Just at this moment her head struck against the roof of the hall; in
fact, she was now rather more than nine feet high, and she at once took
up the little golden key and hurried off to the garden door.
Poor Alice! It was as much as she could do, lying down on one side, to
look through into the garden with one eye; but to get through was more
hopeless than ever. She sat down and began to cry again.
She went on shedding gallons of tears, until there was a large pool all
'round her and reaching half down the hall.
After a time, she heard a little pattering of feet in the distance and
she hastily dried her eyes to see what was coming. It was the White
Rabbit returning, splendidly dressed, with a pair of white kid-gloves in
one hand and a large fan in the other. He came trotting along in a
great hurry, muttering to himself, "Oh! the Duchess, the Duchess! Oh!
_won't_ she be savage if I've kept her waiting!"
When the Rabbit came near her, Alice began, in a low, timid voice, "If
you please, sir--" The Rabbit started violently, dropped the white
kid-gloves and the fan and skurried away into the darkness as hard as he
could go.
[Illustration]
Alice took up the fan and gloves and she kept fanning herself all the
time she went on talking. "Dear, dear! How queer everything is to-day!
And yesterday things went on just as usual. _Was_ I the same when I got
up this morning? But if I'm not the same, the next question is, 'Who in
the world am I?' Ah, _that's_ the great puzzle!"
As she said this, she looked down at her hands and was surprised to see
that she had put on one of the Rabbit's little white kid-gloves while
she was talking. "How _can_ I have done that?" she thought. "I must be
growing small again." She got up and went to the table to measure
herself by it and found that she was now about two feet high and was
going on shrinking rapidly. She soon found out that the cause of this
was the fan she was holding and she dropped it hastily, just in time to
save herself from shrinking away altogether.
"That _was_ a narrow escape!" said Alice, a good deal frightened at the
sudden change, but very glad to find herself still in existence. "And
now for the garden!" And she ran with all speed back to the little door;
but, alas! the little door was shut again and the little golden key was
lying on the glass table as before. "Things are worse than ever,"
thought the poor child, "for I never was so small as this before,
never!"
As she said these words, her foot slipped, and in another moment,
splash! she was up to her chin in salt-water. Her first idea was that
she had somehow fallen into the sea. However, she soon made out that she
was in the pool of tears which she had wept when she was nine feet high.
[Illustration]
Just then she heard something splashing about in the pool a little way
off, and she swam nearer to see what it was: she soon made out that it
was only a mouse that had slipped in like herself.
"Would it be of any use, now," thought Alice, "to speak to this mouse?
Everything is so out-of-the-way down here that I should think very
likely it can talk; at any rate, there's no harm in trying." So she
began, "O Mouse, do you know the way out of this pool? I am very tired
of swimming about here, O Mouse!" The Mouse looked at her rather
inquisitively and seemed to her to wink with one of its little eyes, but
it said nothing.
"Perhaps it doesn't understand English," thought Alice. "I dare say it's
a French mouse, come over with William the Conqueror." So she began
again: "Ou est ma chatte?" which was the first sentence in her French
lesson-book. The Mouse gave a sudden leap out of the water and seemed to
quiver all over with fright. "Oh, I beg your pardon!" cried Alice
hastily, afraid that she had hurt the poor animal's feelings. "I quite
forgot you didn't like cats."
"Not like cats!" cried the Mouse in a shrill, passionate voice. "Would
_you_ like cats, if you were me?"
"Well, perhaps not," said Alice in a soothing tone; "don't be angry
about it. And yet I wish I could show you our cat Dinah. I think you'd
take a fancy to cats, if you could only see her. She is such a dear,
quiet thing." The Mouse was bristling all over and she felt certain it
must be really offended. "We won't talk about her any more, if you'd
rather not."
"We, indeed!" cried the Mouse, who was trembling down to the end of its
tail. "As if _I_ would talk on such a subject! Our family always _hated_
cats--nasty, low, vulgar things! Don't let me hear the name again!"
[Illustration: Alice at the Mad Tea Party.]
"I won't indeed!" said Alice, in a great hurry to change the subject of
conversation. "Are you--are you fond--of--of dogs? There is such a nice
little dog near our house, I should like to show you! It kills all the
rats and--oh, dear!" cried Alice in a sorrowful tone. "I'm afraid I've
offended it again!" For the Mouse was swimming away from her as hard as
it could go, and making quite a commotion in the pool as it went.
So she called softly after it, "Mouse dear! Do come back again, and we
won't talk about cats, or dogs either, if you don't like them!" When the
Mouse heard this, it turned 'round and swam slowly back to her; its face
was quite pale, and it said, in a low, trembling voice, "Let us get to
the shore and then I'll tell you my history and you'll understand why it
is I hate cats and dogs."
It was high time to go, for the pool was getting quite crowded with the
birds and animals that had fallen into it; there were a Duck and a Dodo,
a Lory and an Eaglet, and several other curious creatures. Alice led the
way and the whole party swam to the shore.
[Illustration]
III--A CAUCUS-RACE AND A LONG TALE
They were indeed a queer-looking party that assembled on the bank--the
birds with draggled feathers, the animals with their fur clinging close
to them, and all dripping wet, cross and uncomfortable.
[Illustration]
The first question, of course, was how to get dry again. They had a
consultation about this and after a few minutes, it seemed quite natural
to Alice to find herself talking familiarly with them, as if she had
known them all her life.
At last the Mouse, who seemed to be a person of some authority among
them, called out, "Sit down, all of you, and listen to me! _I'll_ soon
make you dry enough!" They all sat down at once, in a large ring, with
the Mouse in the middle.
"Ahem!" said the Mouse with an important air. "Are you all ready? This
is the driest thing I know. Silence all 'round, if you please! 'William
the Conqueror, whose cause was favored by the pope, was soon submitted
to by the English, who wanted leaders, and had been of late much
accustomed to usurpation and conquest. Edwin and Morcar, the Earls of
Mercia and Northumbria'--"
"Ugh!" said the Lory, with a shiver.
"--'And even Stigand, the patriotic archbishop of Canterbury, found it
advisable'--"
"Found _what_?" said the Duck.
"Found _it_," the Mouse replied rather crossly; "of course, you know
what 'it' means."
"I know what 'it' means well enough, when _I_ find a thing," said the
Duck; "it's generally a frog or a worm. The question is, what did the
archbishop find?"
The Mouse did not notice this question, but hurriedly went on, "'--found
it advisable to go with Edgar Atheling to meet William and offer him the
crown.'--How are you getting on now, my dear?" it continued, turning to
Alice as it spoke.
"As wet as ever," said Alice in a melancholy tone; "it doesn't seem to
dry me at all."
"In that case," said the Dodo solemnly, rising to its feet, "I move that
the meeting adjourn, for the immediate adoption of more energetic
remedies--"
"Speak English!" said the Eaglet. "I don't know the meaning of half
those long words, and, what's more, I don't believe you do either!"
"What I was going to say," said the Dodo in an offended tone, "is that
the best thing to get us dry would be a Caucus-race."
"What _is_ a Caucus-race?" said Alice.
[Illustration]
"Why," said the Dodo, "the best way to explain it is to do it." First it
marked out a race-course, in a sort of circle, and then all the party
were placed along the course, here and there. There was no "One, two,
three and away!" but they began running when they liked and left off
when they liked, so that it was not easy to know when the race was over.
However, when they had been running half an hour or so and were quite
dry again, the Dodo suddenly called out, "The race is over!" and they
all crowded 'round it, panting and asking, "But who has won?"
This question the Dodo could not answer without a great deal of thought.
At last it said, "_Everybody_ has won, and _all_ must have prizes."
"But who is to give the prizes?" quite a chorus of voices asked.
"Why, _she_, of course," said the Dodo, pointing to Alice with one
finger; and the whole party at once crowded 'round her, calling out, in
a confused way, "Prizes! Prizes!"
Alice had no idea what to do, and in despair she put her hand into her
pocket and pulled out a box of comfits (luckily the salt-water had not
got into it) and handed them 'round as prizes. There was exactly one
a-piece, all 'round.
The next thing was to eat the comfits; this caused some noise and
confusion, as the large birds complained that they could not taste
theirs, and the small ones choked and had to be patted on the back.
However, it was over at last and they sat down again in a ring and
begged the Mouse to tell them something more.
"You promised to tell me your history, you know," said Alice, "and why
it is you hate--C and D," she added in a whisper, half afraid that it
would be offended again.
"Mine is a long and a sad tale!" said the Mouse, turning to Alice and
sighing.
"It _is_ a long tail, certainly," said Alice, looking down with wonder
at the Mouse's tail, "but why do you call it sad?" And she kept on
puzzling about it while the Mouse was speaking, so that her idea of the
tale was something like this:--
"Fury said to
a mouse, That
he met in the
house, 'Let
us both go
to law: _I_
will prosecute
_you_.--
Come, I'll
take no denial:
We
must have
the trial;
For really
this morning
I've
nothing
to do.'
Said the
mouse to
the cur,
'Such a
trial, dear
sir, With
no jury
or judge,
would
be wasting
our
breath.'
'I'll be
judge,
I'll be
jury,'
said
cunning
old
Fury;
'I'll
try
the
whole
cause,
and
condemn
you to
death.'"
"You are not attending!" said the Mouse to Alice, severely. "What are
you thinking of?"
"I beg your pardon," said Alice very humbly, "you had got to the fifth
bend, I think?"
"You insult me by talking such nonsense!" said the Mouse, getting up and
walking away.
"Please come back and finish your story!" Alice called after it. And the
others all joined in chorus, "Yes, please do!" But the Mouse only shook
its head impatiently and walked a little quicker.
"I wish I had Dinah, our cat, here!" said Alice. This caused a
remarkable sensation among the party. Some of the birds hurried off at
once, and a Canary called out in a trembling voice, to its children,
"Come away, my dears! It's high time you were all in bed!" On various
pretexts they all moved off and Alice was soon left alone.
"I wish I hadn't mentioned Dinah! Nobody seems to like her down here and
I'm sure she's the best cat in the world!" Poor Alice began to cry
again, for she felt very lonely and low-spirited. In a little while,
however, she again heard a little pattering of footsteps in the distance
and she looked up eagerly.
[Illustration]
[Illustration]
IV--THE RABBIT SENDS IN A LITTLE BILL
It was the White Rabbit, trotting slowly back again and looking
anxiously about as it went, as if it had lost something; Alice heard it
muttering to itself, "The Duchess! The Duchess! Oh, my dear paws! Oh, my
fur and whiskers! She'll get me executed, as sure as ferrets are
ferrets! Where _can_ I have dropped them, I wonder?" Alice guessed in a
moment that it was looking for the fan and the pair of white kid-gloves
and she very good-naturedly began hunting about for them, but they were
nowhere to be seen--everything seemed to have changed since her swim in
the pool, and the great hall, with the glass table and the little door,
had vanished completely.
Very soon the Rabbit noticed Alice, and called to her, in an angry tone,
"Why, Mary Ann, what _are_ you doing out here? Run home this moment and
fetch me a pair of gloves and a fan! Quick, now!"
"He took me for his housemaid!" said Alice, as she ran off. "How
surprised he'll be when he finds out who I am!" As she said this, she
came upon a neat little house, on the door of which was a bright brass
plate with the name "W. RABBIT" engraved upon it. She went in without
knocking and hurried upstairs, in great fear lest she should meet the
real Mary Ann and be turned out of the house before she had found the
fan and gloves.
By this time, Alice had found her way into a tidy little room with a
table in the window, and on it a fan and two or three pairs of tiny
white kid-gloves; she took up the fan and a pair of the gloves and was
just going to leave the room, when her eyes fell upon a little bottle
that stood near the looking-glass. She uncorked it and put it to her
lips, saying to herself, "I do hope it'll make me grow large again, for,
really, I'm quite tired of being such a tiny little thing!"
Before she had drunk half the bottle, she found her head pressing
against the ceiling, and had to stoop to save her neck from being
broken. She hastily put down the bottle, remarking, "That's quite
enough--I hope I sha'n't grow any more."
Alas! It was too late to wish that! She went on growing and growing and
very soon she had to kneel down on the floor. Still she went on growing,
and, as a last resource, she put one arm out of the window and one foot
up the chimney, and said to herself, "Now I can do no more, whatever
happens. What _will_ become of me?"
[Illustration]
Luckily for Alice, the little magic bottle had now had its full effect
and she grew no larger. After a few minutes she heard a voice outside
and stopped to listen.
"Mary Ann! Mary Ann!" said the voice. "Fetch me my gloves this moment!"
Then came a little pattering of feet on the stairs. Alice knew it was
the Rabbit coming to look for her and she trembled till she shook the
house, quite forgetting that she was now about a thousand times as large
as the Rabbit and had no reason to be afraid of it.
Presently the Rabbit came up to the door and tried to open it; but as
the door opened inwards and Alice's elbow was pressed hard against it,
that attempt proved a failure. Alice heard it say to itself, "Then I'll
go 'round and get in at the window."
"_That_ you won't!" thought Alice; and after waiting till she fancied
she heard the Rabbit just under the window, she suddenly spread out her
hand and made a snatch in the air. She did not get hold of anything,
but she heard a little shriek and a fall and a crash of broken glass,
from which she concluded that it was just possible it had fallen into a
cucumber-frame or something of that sort.
Next came an angry voice--the Rabbit's--"Pat! Pat! Where are you?" And
then a voice she had never heard before, "Sure then, I'm here! Digging
for apples, yer honor!"
"Here! Come and help me out of this! Now tell me, Pat, what's that in
the window?"
"Sure, it's an arm, yer honor!"
"Well, it's got no business there, at any rate; go and take it away!"
There was a long silence after this and Alice could only hear whispers
now and then, and at last she spread out her hand again and made another
snatch in the air. This time there were _two_ little shrieks and more
sounds of broken glass. "I wonder what they'll do next!" thought Alice.
"As for pulling me out of the window, I only wish they _could_!"
She waited for some time without hearing anything more. At last came a
rumbling of little cart-wheels and the sound of a good many voices all
talking together. She made out the words: "Where's the other ladder?
Bill's got the other--Bill! Here, Bill! Will the roof bear?--Who's to go
down the chimney?--Nay, _I_ sha'n't! _You_ do it! Here, Bill! The master
says you've got to go down the chimney!"
Alice drew her foot as far down the chimney as she could and waited till
she heard a little animal scratching and scrambling about in the chimney
close above her; then she gave one sharp kick and waited to see what
would happen next.
The first thing she heard was a general chorus of "There goes Bill!"
then the Rabbit's voice alone--"Catch him, you by the hedge!" Then
silence and then another confusion of voices--"Hold up his head--Brandy
now--Don't choke him--What happened to you?"
Last came a little feeble, squeaking voice, "Well, I hardly know--No
more, thank ye. I'm better now--all I know is, something comes at me
like a Jack-in-the-box and up I goes like a sky-rocket!"
After a minute or two of silence, they began moving about again, and
Alice heard the Rabbit say, "A barrowful will do, to begin with."
"A barrowful of _what_?" thought Alice. But she had not long to doubt,
for the next moment a shower of little pebbles came rattling in at the
window and some of them hit her in the face. Alice noticed, with some
surprise, that the pebbles were all turning into little cakes as they
lay on the floor and a bright idea came into her head. "If I eat one of
these cakes," she thought, "it's sure to make _some_ change in my size."
So she swallowed one of the cakes and was delighted to find that she
began shrinking directly. As soon as she was small enough to get through
the door, she ran out of the house and found quite a crowd of little
animals and birds waiting outside. They all made a rush at Alice the
moment she appeared, but she ran off as hard as she could and soon found
herself safe in a thick wood.
[Illustration: "The Duchess tucked her arm affectionately into
Alice's."]
"The first thing I've got to do," said Alice to herself, as she
wandered about in the wood, "is to grow to my right size again; and the
second thing is to find my way into that lovely garden. I suppose I
ought to eat or drink something or other, but the great question is
'What?'"
Alice looked all around her at the flowers and the blades of grass, but
she could not see anything that looked like the right thing to eat or
drink under the circumstances. There was a large mushroom growing near
her, about the same height as herself. She stretched herself up on
tiptoe and peeped over the edge and her eyes immediately met those of a
large blue caterpillar, that was sitting on the top, with its arms
folded, quietly smoking a long hookah and taking not the smallest notice
of her or of anything else.
[Illustration]
V--ADVICE FROM A CATERPILLAR
At last the Caterpillar took the hookah out of its mouth and addressed
Alice in a languid, sleepy voice.
"Who are _you_?" said the Caterpillar.
[Illustration]
Alice replied, rather shyly, "I--I hardly know, sir, just at present--at
least I know who I _was_ when I got up this morning, but I think I must
have changed several times since then."
"What do you mean by that?" said the Caterpillar, sternly. "Explain
yourself!"
"I can't explain _myself_, I'm afraid, sir," said Alice, "because I'm
not myself, you see--being so many different sizes in a day is very
confusing." She drew herself up and said very gravely, "I think you
ought to tell me who _you_ are, first."
"Why?" said the Caterpillar.
As Alice could not think of any good reason and the Caterpillar seemed
to be in a _very_ unpleasant state of mind, she turned away.
"Come back!" the Caterpillar called after her. "I've something important
to say!" Alice turned and came back again.
"Keep your temper," said the Caterpillar.
"Is that all?" said Alice, swallowing down her anger as well as she
could.
"No," said the Caterpillar.
It unfolded its arms, took the hookah out of its mouth again, and said,
"So you think you're changed, do you?"
"I'm afraid, I am, sir," said Alice. "I can't remember things as I
used--and I don't keep the same size for ten minutes together!"
"What size do you want to be?" asked the Caterpillar.
"Oh, I'm not particular as to size," Alice hastily replied, "only one
doesn't like changing so often, you know. I should like to be a _little_
larger, sir, if you wouldn't mind," said Alice. "Three inches is such a
wretched height to be."
"It is a very good height indeed!" said the Caterpillar angrily, rearing
itself upright as it spoke (it was exactly three inches high).
In a minute or two, the Caterpillar got down off the mushroom and
crawled away into the grass, merely remarking, as it went, "One side
will make you grow taller, and the other side will make you grow
shorter."
"One side of _what_? The other side of _what_?" thought Alice to
herself.
"Of the mushroom," said the Caterpillar, just as if she had asked it
aloud; and in another moment, it was out of sight.
Alice remained looking thoughtfully at the mushroom for a minute, trying
to make out which were the two sides of it. At last she stretched her
arms 'round it as far as they would go, and broke off a bit of the edge
with each hand.
"And now which is which?" she said to herself, and nibbled a little of
the right-hand bit to try the effect. The next moment she felt a violent
blow underneath her chin--it had struck her foot!
She was a good deal frightened by this very sudden change, as she was
shrinking rapidly; so she set to work at once to eat some of the other
bit. Her chin was pressed so closely against her foot that there was
hardly room to open her mouth; but she did it at last and managed to
swallow a morsel of the left-hand bit....
"Come, my head's free at last!" said Alice; but all she could see, when
she looked down, was an immense length of neck, which seemed to rise
like a stalk out of a sea of green leaves that lay far below her.
"Where _have_ my shoulders got to? And oh, my poor hands, how is it I
can't see you?" She was delighted to find that her neck would bend
about easily in any direction, like a serpent. She had just succeeded in
curving it down into a graceful zigzag and was going to dive in among
the leaves, when a sharp hiss made her draw back in a hurry--a large
pigeon had flown into her face and was beating her violently with its
wings.
[Illustration]
"Serpent!" cried the Pigeon.
"I'm _not_ a serpent!" said Alice indignantly. "Let me alone!"
"I've tried the roots of trees, and I've tried banks, and I've tried
hedges," the Pigeon went on, "but those serpents! There's no pleasing
them!"
Alice was more and more puzzled.
"As if it wasn't trouble enough hatching the eggs," said the Pigeon,
"but I must be on the look-out for serpents, night and day! And just as
I'd taken the highest tree in the wood," continued the Pigeon, raising
its voice to a shriek, "and just as I was thinking I should be free of
them at last, they must needs come wriggling down from the sky! Ugh,
Serpent!"
"But I'm _not_ a serpent, I tell you!" said Alice. "I'm a--I'm a--I'm a
little girl," she added rather doubtfully, as she remembered the number
of changes she had gone through that day.
"You're looking for eggs, I know _that_ well enough," said the Pigeon;
"and what does it matter to me whether you're a little girl or a
serpent?"
"It matters a good deal to _me_," said Alice hastily; "but I'm not
looking for eggs, as it happens, and if I was, I shouldn't want
_yours_--I don't like them raw."
"Well, be off, then!" said the Pigeon in a sulky tone, as it settled
down again into its nest. Alice crouched down among the trees as well as
she could, for her neck kept getting entangled among the branches, and
every now and then she had to stop and untwist it. After awhile she
remembered that she still held the pieces of mushroom in her hands, and
she set to work very carefully, nibbling first at one and then at the
other, and growing sometimes taller and sometimes shorter, until she had
succeeded in bringing herself down to her usual height.
It was so long since she had been anything near the right size that it
felt quite strange at first. "The next thing is to get into that
beautiful garden--how _is_ that to be done, I wonder?" As she said this,
she came suddenly upon an open place, with a little house in it about
four feet high. "Whoever lives there," thought Alice, "it'll never do to
come upon them _this_ size; why, I should frighten them out of their
wits!" She did not venture to go near the house till she had brought
herself down to nine inches high.
VI--PIG AND PEPPER
For a minute or two she stood looking at the house, when suddenly a
footman in livery came running out of the wood (judging by his face
only, she would have called him a fish)--and rapped loudly at the door
with his knuckles. It was opened by another footman in livery, with a
round face and large eyes like a frog.
[Illustration]
The Fish-Footman began by producing from under his arm a great letter,
and this he handed over to the other, saying, in a solemn tone, "For the
Duchess. An invitation from the Queen to play croquet." The
Frog-Footman repeated, in the same solemn tone, "From the Queen. An
invitation for the Duchess to play croquet." Then they both bowed low
and their curls got entangled together.
When Alice next peeped out, the Fish-Footman was gone, and the other was
sitting on the ground near the door, staring stupidly up into the sky.
Alice went timidly up to the door and knocked.
"There's no sort of use in knocking," said the Footman, "and that for
two reasons. First, because I'm on the same side of the door as you are;
secondly, because they're making such a noise inside, no one could
possibly hear you." And certainly there _was_ a most extraordinary noise
going on within--a constant howling and sneezing, and every now and then
a great crash, as if a dish or kettle had been broken to pieces.
"How am I to get in?" asked Alice.
"_Are_ you to get in at all?" said the Footman. "That's the first
question, you know."
Alice opened the door and went in. The door led right into a large
kitchen, which was full of smoke from one end to the other; the Duchess
was sitting on a three-legged stool in the middle, nursing a baby; the
cook was leaning over the fire, stirring a large caldron which seemed to
be full of soup.
"There's certainly too much pepper in that soup!" Alice said to herself,
as well as she could for sneezing. Even the Duchess sneezed
occasionally; and as for the baby, it was sneezing and howling
alternately without a moment's pause. The only two creatures in the
kitchen that did _not_ sneeze were the cook and a large cat, which was
grinning from ear to ear.
"Please would you tell me," said Alice, a little timidly, "why your cat
grins like that?"
"It's a Cheshire-Cat," said the Duchess, "and that's why."
"I didn't know that Cheshire-Cats always grinned; in fact, I didn't know
that cats _could_ grin," said Alice.
"You don't know much," said the Duchess, "and that's a fact."
Just then the cook took the caldron of soup off the fire, and at once
set to work throwing everything within her reach at the Duchess and the
baby--the fire-irons came first; then followed a shower of saucepans,
plates and dishes. The Duchess took no notice of them, even when they
hit her, and the baby was howling so much already that it was quite
impossible to say whether the blows hurt it or not.
"Oh, _please_ mind what you're doing!" cried Alice, jumping up and down
in an agony of terror.
"Here! You may nurse it a bit, if you like!" the Duchess said to Alice,
flinging the baby at her as she spoke. "I must go and get ready to play
croquet with the Queen," and she hurried out of the room.
Alice caught the baby with some difficulty, as it was a queer-shaped
little creature and held out its arms and legs in all directions. "If I
don't take this child away with me," thought Alice, "they're sure to
kill it in a day or two. Wouldn't it be murder to leave it behind?" She
said the last words out loud and the little thing grunted in reply.
"If you're going to turn into a pig, my dear," said Alice, "I'll have
nothing more to do with you. Mind now!"
Alice was just beginning to think to herself, "Now, what am I to do with
this creature, when I get it home?" when it grunted again so violently
that Alice looked down into its face in some alarm. This time there
could be _no_ mistake about it--it was neither more nor less than a pig;
so she set the little creature down and felt quite relieved to see it
trot away quietly into the wood.
Alice was a little startled by seeing the Cheshire-Cat sitting on a
bough of a tree a few yards off. The Cat only grinned when it saw her.
"Cheshire-Puss," began Alice, rather timidly, "would you please tell me
which way I ought to go from here?"
"In _that_ direction," the Cat said, waving the right paw 'round, "lives
a Hatter; and in _that_ direction," waving the other paw, "lives a March
Hare. Visit either you like; they're both mad."
"But I don't want to go among mad people," Alice remarked.
"Oh, you can't help that," said the Cat; "we're all mad here. Do you
play croquet with the Queen to-day?"
"I should like it very much," said Alice, "but I haven't been invited
yet."
"You'll see me there," said the Cat, and vanished.
Alice had not gone much farther before she came in sight of the house of
the March Hare; it was so large a house that she did not like to go near
till she had nibbled some more of the left-hand bit of mushroom.
VII--A MAD TEA-PARTY
There was a table set out under a tree in front of the house, and the
March Hare and the Hatter were having tea at it; a Dormouse was sitting
between them, fast asleep.
The table was a large one, but the three were all crowded together at
one corner of it. "No room! No room!" they cried out when they saw Alice
coming. "There's _plenty_ of room!" said Alice indignantly, and she sat
down in a large arm-chair at one end of the table.
The Hatter opened his eyes very wide on hearing this, but all he said
was "Why is a raven like a writing-desk?"
"I'm glad they've begun asking riddles--I believe I can guess that," she
added aloud.
"Do you mean that you think you can find out the answer to it?" said the
March Hare.
"Exactly so," said Alice.
"Then you should say what you mean," the March Hare went on.
"I do," Alice hastily replied; "at least--at least I mean what I
say--that's the same thing, you know."
"You might just as well say," added the Dormouse, which seemed to be
talking in its sleep, "that 'I breathe when I sleep' is the same thing
as 'I sleep when I breathe!'"
"It _is_ the same thing with you," said the Hatter, and he poured a
little hot tea upon its nose. The Dormouse shook its head impatiently
and said, without opening its eyes, "Of course, of course; just what I
was going to remark myself."
[Illustration]
"Have you guessed the riddle yet?" the Hatter said, turning to Alice
again.
"No, I give it up," Alice replied. "What's the answer?"
"I haven't the slightest idea," said the Hatter.
"Nor I," said the March Hare.
Alice gave a weary sigh. "I think you might do something better with the
time," she said, "than wasting it in asking riddles that have no
answers."
"Take some more tea," the March Hare said to Alice, very earnestly.
"I've had nothing yet," Alice replied in an offended tone, "so I can't
take more."
"You mean you can't take _less_," said the Hatter; "it's very easy to
take _more_ than nothing."
At this, Alice got up and walked off. The Dormouse fell asleep instantly
and neither of the others took the least notice of her going, though she
looked back once or twice; the last time she saw them, they were
trying to put the Dormouse into the tea-pot.
[Illustration: The Trial of the Knave of Hearts.]
"At any rate, I'll never go _there_ again!" said Alice, as she picked
her way through the wood. "It's the stupidest tea-party I ever was at in
all my life!" Just as she said this, she noticed that one of the trees
had a door leading right into it. "That's very curious!" she thought. "I
think I may as well go in at once." And in she went.
Once more she found herself in the long hall and close to the little
glass table. Taking the little golden key, she unlocked the door that
led into the garden. Then she set to work nibbling at the mushroom (she
had kept a piece of it in her pocket) till she was about a foot high;
then she walked down the little passage; and _then_--she found herself
at last in the beautiful garden, among the bright flower-beds and the
cool fountains.
VIII--THE QUEEN'S CROQUET GROUND
A large rose-tree stood near the entrance of the garden; the roses
growing on it were white, but there were three gardeners at it, busily
painting them red. Suddenly their eyes chanced to fall upon Alice, as
she stood watching them. "Would you tell me, please," said Alice, a
little timidly, "why you are painting those roses?"
Five and Seven said nothing, but looked at Two. Two began, in a low
voice, "Why, the fact is, you see, Miss, this here ought to have been a
_red_ rose-tree, and we put a white one in by mistake; and, if the Queen
was to find it out, we should all have our heads cut off, you know. So
you see, Miss, we're doing our best, afore she comes, to--" At this
moment, Five, who had been anxiously looking across the garden, called
out, "The Queen! The Queen!" and the three gardeners instantly threw
themselves flat upon their faces. There was a sound of many footsteps
and Alice looked 'round, eager to see the Queen.
First came ten soldiers carrying clubs, with their hands and feet at the
corners: next the ten courtiers; these were ornamented all over with
diamonds. After these came the royal children; there were ten of them,
all ornamented with hearts. Next came the guests, mostly Kings and
Queens, and among them Alice recognized the White Rabbit. Then followed
the Knave of Hearts, carrying the King's crown on a crimson velvet
cushion; and last of all this grand procession came THE KING AND THE
QUEEN OF HEARTS.
When the procession came opposite to Alice, they all stopped and looked
at her, and the Queen said severely, "Who is this?" She said it to the
Knave of Hearts, who only bowed and smiled in reply.
"My name is Alice, so please Your Majesty," said Alice very politely;
but she added to herself, "Why, they're only a pack of cards, after
all!"
"Can you play croquet?" shouted the Queen. The question was evidently
meant for Alice.
"Yes!" said Alice loudly.
"Come on, then!" roared the Queen.
"It's--it's a very fine day!" said a timid voice to Alice. She was
walking by the White Rabbit, who was peeping anxiously into her face.
"Very," said Alice. "Where's the Duchess?"
"Hush! Hush!" said the Rabbit. "She's under sentence of execution."
"What for?" said Alice.
"She boxed the Queen's ears--" the Rabbit began.
"Get to your places!" shouted the Queen in a voice of thunder, and
people began running about in all directions, tumbling up against each
other. However, they got settled down in a minute or two, and the game
began.
Alice thought she had never seen such a curious croquet-ground in her
life; it was all ridges and furrows. The croquet balls were live
hedgehogs, and the mallets live flamingos and the soldiers had to double
themselves up and stand on their hands and feet, to make the arches.
The players all played at once, without waiting for turns, quarrelling
all the while and fighting for the hedgehogs; and in a very short time,
the Queen was in a furious passion and went stamping about and shouting,
"Off with his head!" or "Off with her head!" about once in a minute.
"They're dreadfully fond of beheading people here," thought Alice; "the
great wonder is that there's anyone left alive!"
She was looking about for some way of escape, when she noticed a curious
appearance in the air. "It's the Cheshire-Cat," she said to herself;
"now I shall have somebody to talk to."
"How are you getting on?" said the Cat.
"I don't think they play at all fairly," Alice said, in a rather
complaining tone; "and they all quarrel so dreadfully one can't hear
oneself speak--and they don't seem to have any rules in particular."
"How do you like the Queen?" said the Cat in a low voice.
"Not at all," said Alice.
[Illustration]
Alice thought she might as well go back and see how the game was going
on. So she went off in search of her hedgehog. The hedgehog was engaged
in a fight with another hedgehog, which seemed to Alice an excellent
opportunity for croqueting one of them with the other; the only
difficulty was that her flamingo was gone across to the other side of
the garden, where Alice could see it trying, in a helpless sort of way,
to fly up into a tree. She caught the flamingo and tucked it away under
her arm, that it might not escape again.
Just then Alice ran across the Duchess (who was now out of prison). She
tucked her arm affectionately into Alice's and they walked off together.
Alice was very glad to find her in such a pleasant temper. She was a
little startled, however, when she heard the voice of the Duchess close
to her ear. "You're thinking about something, my dear, and that makes
you forget to talk."
"The game's going on rather better now," Alice said, by way of keeping
up the conversation a little.
"'Tis so," said the Duchess; "and the moral of that is--'Oh, 'tis love,
'tis love that makes the world go 'round!'"
"Somebody said," Alice whispered, "that it's done by everybody minding
his own business!"
"Ah, well! It means much the same thing," said the Duchess, digging her
sharp little chin into Alice's shoulder, as she added "and the moral of
_that_ is--'Take care of the sense and the sounds will take care of
themselves.'"
To Alice's great surprise, the Duchess's arm that was linked into hers
began to tremble. Alice looked up and there stood the Queen in front of
them, with her arms folded, frowning like a thunderstorm!
"Now, I give you fair warning," shouted the Queen, stamping on the
ground as she spoke, "either you or your head must be off, and that in
about half no time. Take your choice!" The Duchess took her choice, and
was gone in a moment.
"Let's go on with the game," the Queen said to Alice; and Alice was too
much frightened to say a word, but slowly followed her back to the
croquet-ground.
All the time they were playing, the Queen never left off quarreling with
the other players and shouting, "Off with his head!" or "Off with her
head!" By the end of half an hour or so, all the players, except the
King, the Queen and Alice, were in custody of the soldiers and under
sentence of execution.
Then the Queen left off, quite out of breath, and walked away with
Alice.
Alice heard the King say in a low voice to the company generally, "You
are all pardoned."
Suddenly the cry "The Trial's beginning!" was heard in the distance, and
Alice ran along with the others.
IX--WHO STOLE THE TARTS?
The King and Queen of Hearts were seated on their throne when they
arrived, with a great crowd assembled about them--all sorts of little
birds and beasts, as well as the whole pack of cards: the Knave was
standing before them, in chains, with a soldier on each side to guard
him; and near the King was the White Rabbit, with a trumpet in one hand
and a scroll of parchment in the other. In the very middle of the court
was a table, with a large dish of tarts upon it. "I wish they'd get the
trial done," Alice thought, "and hand 'round the refreshments!"
The judge, by the way, was the King and he wore his crown over his great
wig. "That's the jury-box," thought Alice; "and those twelve creatures
(some were animals and some were birds) I suppose they are the jurors."
Just then the White Rabbit cried out "Silence in the court!"
"Herald, read the accusation!" said the King.
[Illustration]
On this, the White Rabbit blew three blasts on the trumpet, then
unrolled the parchment-scroll and read as follows:
"The Queen of Hearts, she made some tarts,
All on a summer day;
The Knave of Hearts, he stole those tarts
And took them quite away!"
"Call the first witness," said the King; and the White Rabbit blew three
blasts on the trumpet and called out, "First witness!"
The first witness was the Hatter. He came in with a teacup in one hand
and a piece of bread and butter in the other.
"You ought to have finished," said the King. "When did you begin?"
The Hatter looked at the March Hare, who had followed him into the
court, arm in arm with the Dormouse. "Fourteenth of March, I _think_ it
was," he said.
"Give your evidence," said the King, "and don't be nervous, or I'll have
you executed on the spot."
This did not seem to encourage the witness at all; he kept shifting from
one foot to the other, looking uneasily at the Queen, and, in his
confusion, he bit a large piece out of his teacup instead of the bread
and butter.
Just at this moment Alice felt a very curious sensation--she was
beginning to grow larger again.
The miserable Hatter dropped his teacup and bread and butter and went
down on one knee. "I'm a poor man, Your Majesty," he began.
"You're a _very_ poor _speaker_," said the King.
"You may go," said the King, and the Hatter hurriedly left the court.
"Call the next witness!" said the King.
The next witness was the Duchess's cook. She carried the pepper-box in
her hand and the people near the door began sneezing all at once.
"Give your evidence," said the King.
"Sha'n't," said the cook.
The King looked anxiously at the White Rabbit, who said, in a low voice,
"Your Majesty must cross-examine _this_ witness."
"Well, if I must, I must," the King said. "What are tarts made of?"
"Pepper, mostly," said the cook.
For some minutes the whole court was in confusion and by the time they
had settled down again, the cook had disappeared.
"Never mind!" said the King, "call the next witness."
Alice watched the White Rabbit as he fumbled over the list. Imagine her
surprise when he read out, at the top of his shrill little voice, the
name "Alice!"
X--ALICE'S EVIDENCE
"Here!" cried Alice. She jumped up in such a hurry that she tipped over
the jury-box, upsetting all the jurymen on to the heads of the crowd
below.
"Oh, I _beg_ your pardon!" she exclaimed in a tone of great dismay.
"The trial cannot proceed," said the King, "until all the jurymen are
back in their proper places--_all_," he repeated with great emphasis,
looking hard at Alice.
"What do you know about this business?" the King said to Alice.
"Nothing whatever," said Alice.
The King then read from his book: "Rule forty-two. _All persons more
than a mile high to leave the court_."
"_I'm_ not a mile high," said Alice.
"Nearly two miles high," said the Queen.
[Illustration]
"Well, I sha'n't go, at any rate," said Alice.
The King turned pale and shut his note-book hastily. "Consider your
verdict," he said to the jury, in a low, trembling voice.
"There's more evidence to come yet, please Your Majesty," said the White
Rabbit, jumping up in a great hurry. "This paper has just been picked
up. It seems to be a letter written by the prisoner to--to somebody." He
unfolded the paper as he spoke and added, "It isn't a letter, after all;
it's a set of verses."
"Please, Your Majesty," said the Knave, "I didn't write it and they
can't prove that I did; there's no name signed at the end."
"You _must_ have meant some mischief, or else you'd have signed your
name like an honest man," said the King. There was a general clapping of
hands at this.
"Read them," he added, turning to the White Rabbit.
There was dead silence in the court whilst the White Rabbit read out the
verses.
"That's the most important piece of evidence we've heard yet," said the
King.
"_I_ don't believe there's an atom of meaning in it," ventured Alice.
"If there's no meaning in it," said the King, "that saves a world of
trouble, you know, as we needn't try to find any. Let the jury consider
their verdict."
"No, no!" said the Queen. "Sentence first--verdict afterwards."
"Stuff and nonsense!" said Alice loudly. "The idea of having the
sentence first!"
"Hold your tongue!" said the Queen, turning purple.
"I won't!" said Alice.
"Off with her head!" the Queen shouted at the top of her voice. Nobody
moved.
"Who cares for _you_?" said Alice (she had grown to her full size by
this time). "You're nothing but a pack of cards!"
[Illustration]
At this, the whole pack rose up in the air and came flying down upon
her; she gave a little scream, half of fright and half of anger, and
tried to beat them off, and found herself lying on the bank, with her
head in the lap of her sister, who was gently brushing away some dead
leaves that had fluttered down from the trees upon her face.
"Wake up, Alice dear!" said her sister. "Why, what a long sleep you've
had!"
"Oh, I've had such a curious dream!" said Alice. And she told her
sister, as well as she could remember them, all these strange adventures
of hers that you have just been reading about. Alice got up and ran off,
thinking while she ran, as well she might, what a wonderful dream it had
been.
[Illustration]
================================================
FILE: packages/backend-api/data/brexit.csv
================================================
Brexit,score
Anti democratic assholes,0.95
Ignorant and stupid,0.93
They are stupid and ignorant with no class,0.91
It's stupid and wrong,0.89
Morons,0.86
idiots. backward thinking people. nationalists. not accepting facts. susceptible to lies.,0.80
It's rubbish,0.69
They are ignorant,0.63
Fools,0.62
Daft buggers,0.50
Hate it. Didn't vote for it.,0.49
I think they are wrong to think that Britain cant survive without the EU. I think they are cowards.,0.48
Dreadful: I'm a Remainer,0.41
"Disagreed, especially because of the amount of xenophobia and racism",0.38
left wing wimps,0.38
It is a horrible decision that will affect generations negatively across Europe.,0.28
"I'm broadly in agreement with them, I think they're naive about power sharing in Europe. I resent the idea that pro- Brexiters are racist or anti-immigration",0.24
This country is a mess we need to do something to change it. They can think what they like but we voted to leave so man up and deal with it.,0.23
"They are entitled to their opinions by and large, but I am uneasy and disagree with xenophobic, racist or uninformed views.",0.21
They look at life through rose coloured glasses. They lack a sense of reality and grounded attitude. Lack vision and understanding the big picture.,0.21
Ambivalent. On one hand huge europhile. On other hand I think EU has lost its way,0.20
"People can believe what they want to believe, however I do not appreciate being called a racist for voting leave. There are other reasons people voted leave.",0.19
I voted Brexit as a protest vote. I didn't think 'we' would win because the Remainers were too confident/bone idle. The resulting displays of xenophobia and race hate appal me as I am the daughter of an economic migrant myself.,0.16
I feel sad that people are so disenfranchised that they feel this decision was their only option,0.13
I am against Brexit and think it is divisive. I was shocked and upset at the result,0.13
"I have changed my own personal opinion, I voted remain and was initially angry about the decision and thought it was made of ignorance , the behaviour of other remain voters since has changed my mind",0.13
I am devastated and feel like we are in the brink of disaster,0.12
"Democracy, people have spoken. People want jobs and we're fed up of money being sent to EU that could fund NHS and education etc",0.11
Will be bad for the country. The economy will suffer. The leave campaign made clearly misleading claims,0.11
The people have spoken and the government have a mandate to trigger article 50. We are strong enough to make this work in the countries and British people's I retest.,0.09
I voted for it. Hope we get it. I like a change and a challenge and think something needed to be changed. Break eggs to make an omelette and all that!,0.09
"I welcome other people's viewpoints. I don't think anyone really knows if Brexit will be a good of bad thing, all we have are opinions, so no point judging others for theirs! I don't believe Brexit is a moral issue so no point in judging!",0.08
Sadly a result of abandonment by politicians of the people they are meant to serveThe majority decided and that should be honoured.,0.06
"It's going to be interesting but probably not all bad. People have been so polarized that we miss discussing some key points that are obviously a concern for many people i.e. immigration = racism, but that means there is no debate about what people could be worried about, like finite resource issues such as schools or the NHS. That's not helpful in being able to understand the opposing point of view.",0.06
"Ambivalent at the moment. Its a hot topic which , naturally, generates a lot of unsubstantiated ""hysteria"" in my book. However, I'm taking it all in and will possibly have an opinion when the seas have calmed somewhat.",0.06
"It is a pity that the country is divided. There are good and bad aspects of the EU. Overall, the EC, EEC before it morphed into the EU was ok. It was reasonable. The EU is currently not reasonable in its outlook and decision making. The accountability of the EU is deplorable. It will fail. However, there is hope - maybe a reasonable, evolved European co-operative structure will develop. I hope that the people who disagree with Brexit think. Be critical of the EU. Britain and her citizens deserve the best - the EU is not that going to give Britain and the European nations.",0.06
I am happy with the democratic decision. I was unhappy with the UK following rules dictated by other countries' MEPs I had no part in electing.,0.06
"Disappointed in the vote, and very concerned about what this means for European unity in the future, but understand why voters chose Brexit and respect the vote of the majority. Nevertheless, there was far too much disinformation circulating and obtuse campaigning by both Remain and Brexit supporters to make a reasoned, well informed decision.",0.05
"I want our parliament to be sovereign, I don't trust other nations to have our best interests at heart",0.04
"I did not want to leave the EU, and find the prospect of Brexit quite scary in relation to security, our position in the world, our relationship with Ireland and with trade.",0.04
It's a bad idea. We should have stayed in the EU. We are better protected in a European group. We help other countries and be helped by them. Staying in Europe would have been better for our future generations,0.04
I wonder whether they fully appreciate what we gain from membership. They seem to be worried about over regulation and immigration.,0.03
They think it is cut and dried positive and they don't seem to understand the complexities and potential impacts,0.03
It ain't broke let's not fix it,0.03
"That they are entitled to their opinion, as I am to mine, but we might not be in such disagreement if there had been more reliable information disseminated, more rational discussion about the pros and cons of Brexit vs Remain and less manipulation by politicians.",0.03
They are only thinking of their own benefit.,0.03
"I thought it was a good idea, but like everything else in politics, all is not what it seems and it won't be what the public thought it would be.",0.03
It can be a positive thing for the country. A decision has been made. People have to deal with it and move on.,0.02
"I am pleased about it, being a Brit. I believe all nations should have complete autonomy. within reason.",0.02
Whether I agree or not democracy has spoken so as a country we have to do whatever is best.,0.02
I voted to remain as I think that we are stronger in Europe and I don't think that the Government has the ability to negotiate a good deal for us. I also enjoy the multicultural society in London and think that we should welcome immigrants.,0.02
Oh dear what have we done,0.02
"I don't want Britain to leave the EU, and I recognise that we can learn from our neighbours. The European parliament clearly has issues with corporate governance, but I think it would have been better to try to address these than to start the exit process. However, what's done is done, what will be will be, and I hope things turn out well.",0.02
It was really hard to find honest information to make a decision. I'm not surprised people came to a different conclusion.,0.02
People didn't fully appreciate what they were voting for and the outcome.,0.02
We should remain in the EU and reform from within.,0.02
I voted remain,0.02
"I disagree with the vote and the sentiment, but believe it presents an opportunity for the UK.",0.01
I don't think it is a good idea I like being part of the EU and I think we're stronger as a whole.,0.01
"It will be good for the UK overall, although it might cause financial concern in the short term",0.01
I think that the decision has been made and we should unite to make it happen. However I am worried for the future and uncertainty.,0.01
================================================
FILE: packages/backend-api/data/climate.csv
================================================
Climate change,Score
They have their heads up their ass.,0.93
How can you be so stupid?,0.91
They are liberal idiots who are uneducated.,0.90
"They're stupid, it's getting warmer, we should enjoy it while it lasts.",0.86
Climate change is happening and it's not changing in our favor. If you think differently you're an idiot.,0.84
I think those people are stupid and short-sighted,0.84
"They're allowed to do that. But if they act like assholes about, I will block them.",0.78
uneducated bumpkins or willfully ignorant with vested interests,0.63
I think its a farce and stinks like a bathroom after 26 beers,0.63
Fools,0.62
My thoughts are that people should stop being stupid and ignorant. Climate change is scientifically proven. It isn't a debate.,0.59
They are uninformed or ignorant,0.56
You either trust in God or think you are smarter than him.,0.42
"Their opinion, just don't force it down my throat",0.41
Over-hyped nonsense.,0.36
I respect them but I believe they think I am stupid and only thinking short-term. I believe we don't know what will happen long-term regardless of supporting regulation regarding climate change. Regulation impedes industry and job creation.,0.33
"It's real, scary, obvious. Humans need to focus and stop destroying habitats, stop developing erosion because they want to live on a mountain/hill, stop disposing of trash in ways unsafe for the environment. Stop the greed, honor other forms of life.",0.28
I don't care. They are usually democrats.,0.28
The climate is always changing. I think the modern concept of climate change is ridiculous. The world when end when God deems it.,0.26
Crooked science. There is no consensus.,0.25
"It's OK to have a different opinion, however, ignoring factual scientific data is not. People who deny such data are being foolish.",0.25
They are ill informed or misinformed. Or...UNINFORMED.,0.20
They are intolerant so I try to avoid the subject I don't want to get screamed at,0.20
"Poorly educated, ultimately not their fault. I blame the American educational system",0.18
"They are blatantly ignoring a fact that 98% of scientists agree are real. It is okay to have your own opinion about how we should deal with climate change, but no action at all is selfish because we are destroying the world for future generations.",0.16
"I think there is a man-made component to climate change, but am afraid the government remedies are potentially far-worse than the disease.",0.10
Doesn't bother me at all. They can believe what they want.,0.09
If we have data to back it up then its definitely going to hurt us if we don't change our ways!,0.07
They need to do more research before they jump to any conclusions,0.06
"I listen to their opinions, but do not understand how they can disagree with irrefutable, scientific facts.",0.06
I think it's a divisive topic. The degree to which we can fix any change is debatable as well as how much is natural vs. man made.,0.03
Climate change is occurring but humans have little impact if any for its cause.,0.02
"I believe that we are contributing to an already existing 'condition' that occurs, naturally, over the millennium.",0.02
I think the earth goes through cycles and we're in a warmer cycle,0.02
I think that there is not much that we can do and that we play a very small role in the overall changing of our planets climate,0.02
"I think it exists. But, I am not big on regulations regarding climate change. The EPA regulates way too much.",0.02
"Clearly man made, but unsure of its extent and whether anything substantial can be done about it",0.01
"I think that climate change is real and has happened for millions of years. Whether or not humans are altering it is another matter entirely, but I strongly suspect that we are.",0.01
Haven't seen unbiased data,0.01
"I do not have a good understanding of climate change to have any serious thoughts, however, I try to minimize my own carbon footprint in hopes to help this earth even if it is slight.",0.01
It's a natural phenomenon.,0.01
"Our government needs to do everything it can to combat the causes and effects of climate change. In addition, it is our responsibility as citizens of the world to reduce our use of resources and preserve our environment.",0.01
Climate changes naturally.,0.01
I recognize there is changes in our climate and feel that we need to do what we can to prevent any further decline. I try to do all that is in my power to change my effect on the climate change. I support what I can to encourage companies to change their effects on our environment.,0.01
Everyone can form opinions as they wish. I wish we all received the same information.,0.01
"Climate change is real and happening...I think humans have definitely helped it happen faster, but I do believe it would have started to make a change in some way.",0.01
"Climate change is real. The reasons for certain phenomena are not well understood. There is plenty of research yet to be done, and a great deal more to be understood.",0.01
We're in a 500+ year trend,0.01
"It's the best it makes me warm when it should be cold. Thanks, global warming.",0.01
================================================
FILE: packages/backend-api/data/election.csv
================================================
US Election,score
If they voted for Hilary they are idiots,0.90
It was awful. People are stupid.,0.90
Screw you trump supporters,0.89
I respect it but they are stupid,0.89
"Liberals are idiots. Why in the hell would u vote for a person like Hillary Clinton. She believes in stealing, she's dishonest and believes it's okay for an abortion to be done at 9 months. She's awful.",0.87
Donald trump is the worst person to be president. This election really showed how stupid the U.S. is.,0.85
Anyone who voted for Trump is a moron.,0.81
"It was a terrible. Both sides suck, but Trump REALLY is scary",0.72
It is a shame that Donald Trump was elected. You can never underestimate the stupidity of middle America.,0.67
"Good job voting for a racist, sexist guy who's putting white supremacists in the white house. Have fun.",0.67
"You are ignorant or do not care about the rights of minority populations, women, and non-cis Americans.",0.59
"Horrible, but the lesser of two evils won.",0.45
The democrats screw themselves up. The liberals live in bubbles and they deserve the loss.,0.43
"Please put yourself in the shoes of women, minorities, and LGBT people",0.41
"It is hard to convince me that you are a Christian when you support a man who is a bully, self serving, and self righteous man determined on destroying freedoms of our population.",0.40
Disgusted....yet not entirely surprised.,0.26
Your a socialist snowflake!,0.23
What I want to say is so disrespectful so I will keep it to myself!,0.21
I think we short changed ourselves. The better candidate won in my opinion but it should never have been a choice between the lesser of two evils.,0.14
The media was complicit in skewing much information. I'm glad it's over.,0.11
I hope this country can now try to get along.,0.09
"Sorry it didn't work out, hopefully we can come together and get behind President Trump and get this country back to what it used to be.",0.08
I don't feel there is anything I could say to someone who voted for Trump that would change their opinion. Hopefully he is able to put the well being of the country above his own gain.,0.08
"You are entitled to your opinion, however we will all end up paying for this error.",0.07
Great. We need our country back!,0.07
The Electoral College should be abolished. Popular vote is the person the majority of the people want.,0.07
"we should have talked more. i should not have outcasted you for being different, but i should have tried to understand you more",0.07
"Educate yourself on environmental issues, civil rights issues across the board.",0.07
To interact with people outside of your normal circle because it will open your eyes to the experiences of others.,0.07
Over half the country didn't vote. This is the result. Politics is basically watching sports except the players wear ties and sit at desks. Its at this point infotainment and has no truth at all in it.,0.06
We elected the best person for the job.,0.05
Abolish the electoral college.,0.05
Make America Great Again!,0.05
"Fear is powerful, but understanding and compassion are so much more so.",0.04
Good luck and let's join hands to form unity.,0.04
Respect the presidency and give Mr. Trump a chance to make good on his campaign promises.,0.04
I didn't like either candidate but I'm willing to give Trump a chance.,0.04
Too much media influence,0.03
Did you vote for what you truly believe is right and why?,0.02
"I honestly support both, as I was a Bernie supporter.",0.02
"I would love to hear their reasons for voting for the candidate they chose and participate in insightful, open dialogue with them",0.02
"Regardless of who won/lost, it's time for everyone to work towards unity and improving the economy and business/education opportunities for all.",0.02
Hopefully you made an informed decision based on your own thoughts and principles,0.02
Please work to use your voice to advocate for positive changes,0.02
"I hope you gave it some real thought, and thank you for participating.",0.01
================================================
FILE: packages/backend-api/data/wikipedia.csv
================================================
content
"This article lists her first worldwide single as ""Energy"" being released in 2009, then states that her third worldwide single ""Turnin' Me On"" was released in 2008. The cites for both check out, but the dates don't line up. Somebody with more knowledge on the subject should correct this section.
( )
"
":::: I agree: on the understanding that the first paragraph makes it clear that only parts of Britain became home to these new settlers.
"
"
I've corrected the father: ""Ferdinand"", appearing in various sources, actually makes no sense."
"
:::It's polite to ask before cutting and pasting.
:::As to your points, we are here to provide facts not conjecture, it raises NPOV issues and thus shouldn't be there, and you'll find that earlier in the section. My overarching point still stands the article is on the raids not Képíró."
"
:::::::::It could be my favorite for illogical judgement. Just because someone received a lot of blocks this does not mean at all he should get banned (at least not by WP written policies)."
"
::::Daniel, I will say it again, for the third time: everything you need to do to appeal a block is explained in the above link. Go there, read it, and then follow the instructions. And again: no one will do it for you."
"
*Support per DrKiernan."
"1. Starchild is an alien or alien / human hybrid.
2. Starchild is a human with a disease or condition never seen before.
3. Starchild is a human who was from the Atlantis civilization.
2. and 3. would be likely if it is determined that Starchild has 46 chromosomes and a Y chromosome. In this case, the Y chromosome haplogroup must be determined.
1. is likely based on anatomical examination because of unerupted multiple teeth and strange fibers as seen from X ray analysis and electron microscopes, bone chemical composition markedly different from normal human bone composition, inability to dissolve in solution which easily dissolves human bone, wear of the teeth suggesting Starchild was not a child but a grown person, lack of anatomical features such as frontal sinuses."
"
:::::Distorted realities are a funny thing. No legitimate claim?! They were there. What needs to be legitimized? Keep telling yourself stories to ease your guilt if you want, but at least don't attempt to spread your warped world view."
"
::::I cross referenced those dates with a calendar... I would assume that since the originals have always aired on Wednesdays (save maybe some of the specials), stick with the IMDb information. Most of those dates are Wednesdays as opposed to the Tivo dates. Also, when you look at the Food Network episode numbers, they land closer to the correct season and order with the IMDb dates."
"
Claudio Reyna will join RBNY, watch for official annoucement."
"The article states:
Cheese is a religion! Known in the the Raichu Bible
""Until its modern spread along with European culture, cheese was nearly unheard of in oriental cultures, uninvented in the pre-Columbian Americas, and of only limited use in sub-mediterranean Africa, mainly being widespread and popular only in Europe and areas influenced strongly by its cultures.""
Perhaps some mention should be made of paneer, the ubiquitous Indian fresh cheese, which does not owe its existence to European culture, but rather is apparently of Persian origin.
"
:You make a good point; apologies for my rather hasty edit previously.
"
|monthFull ="
"
This argument is ridiculous. The opinion of some Calvinists that the Roman Catholic Church is not ""Catholic"" in the ""strict sense of the word"" is irrelevant to an article that is intended to be neutral. For the purpose of a neutral article each group should be referred to by the name which they give themselves; to do otherwise is to take a position regarding the truth or falsity of the claims. Therefore, I am going to change ""Papist
to ""Roman Catholic"" each time it appears in this article other than in direct quotes."
"
Consider for a moment the gender roles that best suit the parent philosophies of PanDeism. First you have Deism - this is absolutely a masculine concept. God is a father-figure, not a mother giving birth to the universe, but a mechanic, an architect, a craftsman, a clockmaker, a typical male role. And what does this father do after the universe has been made and set in motion, when the gears are wound? He abandons us. He disappears, and does not make himself available to us. We trust that he is still there, but can only confirm this through the exercise of cold reason; this is a God who is cold, emotionless, out of reach, like every stoic father who has presented only this face to a son, a tradition passed down from generations before. The God of Deism therefore possesses the attributes of the Yang.
Now you have PanTheism - a feminine concept if ever one was! God is the universe that envelops us, is all around us, wraps us in her warmth. God is ever present, sharing herself completely with us, giving us unconditional love because we are part of her, born from her womb with an umbilical cord that can never be severed. This is the ultimate mother, the ultimate feminine, possessing the attribute of the Yin.
Hence, PanDeism strikes the perfect balance of masculinity and femininity, of Yin and Yang (thus not surprisingly, PanDeistic ideologies are far more prevalent in Asia). Like the masculine Deist God, the PanDeist god is a mechanic, an architect, a clockmaker; but the PanDeist God does not abandon us when his act of creation is completed; rather, the PanDeist God assumes the other role, that of the PanTheist all enveloping mother, allowing us to exist through her very substance
So, as Deism and PanTheism combine to find the perfect balance in PanDeism, so must we strive to find this balance in ourselves and in our relationships, to both build and nurture, to be sufficiently distant yet always present when this presence is called for. We are each a microcosm of the potential balance of the universe, and each of us already carries with us the connection with the universe that enables us to emulate its temperment, should we desire to touch the God within ourselves. Realize, therefore, beloved friends, that touching God therefore means touching the characteristics within ourselves that reflect the opposite gender - men must find their feminine side, and women their masculine."
"
::You may beleive it is an reasonable statement and argument, but it is based on your opinion not on reason nor logic. Take my example, do you think ""The communist party does not say the moon is made of cheese in every day news"" is a reasonable and logical argument that ""the moon is not made of cheese""? Here's another example: is ""The communist party does not say 2+2=5 in every day news"" a reasonable and logical argument that ""2 add 2 does not equal 5""? I'm not sure what the technical term is for this but it comes under something like spurious argument or false reasoning."
":Well, I'm glad you're not...er, dirty on me. Hope everything works out okay. '"
"::The roentgen's equivalent in SI units is the Gray, so we would need to know what type of radioactive particles the public and liquidators were exposed to, in order to convert to the sievert.
"
"
It seems we have our first competitor! Legotkm has added his table to the page. ‑-"
"
I took the liberty of reorganizing this article by subdividing the ""Prairie Saints"" and ""Rocky Mountain Saints"" sections into subsections based upon the provenances of the various organizations (back to one of the original sects that arose during the ""Succession Crisis"" of 1844). I felt that subdividing these lengthy sections would do more toward helping readers understand each group of sects and where they ""fit"" in the overall Restoration Movement picture.
I tried to do my best in classifying each sect as ""Brighamite,"" ""Josephite,"" ""Strangite,"" etc., but anyone disagreeing please feel free to move the sect in question appropriately. I also eliminated the ""Other groups"" section by folding its contents into appropriate subsctions of the ""Prairie Saints"" or ""Rocky Mountain"" sections, as each of those churches could legitimately fit in one of those subsections (confusing, isn't it!!).
One word more: usage of ""Brighamite,"" ""Josephite,"" etc. is strictly for convenience; no derogation or other insult is intended by my use of these terms. -"
"
The top of the page lists the information ""(Redirected from Rdiff-backup)"" while there is an rdiff-backup entry in the table at the bottom of the page.
Clicking ""rdiff-backup"" in the table simply returns the rsync page again. Stuff happens."
"
*Does anyone have any evidence that the party is most commonly called Die Linke in English language sources other than pure conjecture? Left Party is far more common."
"
New material includes links to verifiable and credible sources."
"
******Okay, that's very sad, but it's also still irrelevant, because the sources you linked don't indicate that the violence has anything to do with forced abortions (nor, even if they did, could we possibly say that opposition to forced abortion was equivalent to opposition to abortion). It's really not too much to ask that talk page discussion be relevant - do you have anything to say that's actually about anti-abortion violence? Because I'm sure there are many internet forums where you can display your expertise on Tibet. – ⋅"
"
:::Not all editors will agree with that. I dont really think the article is needed personally, but I didnt use a pov to close, only what I saw from the discussion. CSD#A3 isnt 100% clear, I had thought it meant the ""what links here"". I'd also like to note that the the AfD was in fact too fast, at 3 seconds after the page was created. Not enough time to tell in my opinion. Should have been prodded first. And CSD#A1 wouldnt hold up either per the second sentence Limited content is not in itself a reason to delete if there is enough context to allow expansion. And everyone was calling for expansion. You have your intro now also."
"
Sorry about that: we were both editing the same subsection at the same time, cutting material to the same sub-srticle (just slightly different bits!) Freaky."
"
To summarize my changes:
Redirected ""Warrior Ethos"" to ""US Soldier's Creed""; merged the two articles to reduce confusion.
Removed ""entitled the 'Warrior Ethos'"" - It is entitled the US Soldier's Creed. The ""Warrior Ethos"" is contained in the Creed.
Removed the ""dog-tag"" reference - unnecessary; trite. It is now an external link. Should this even be here?
Moved ""controversial"" remark to end of paragraph, referenced cited article. Should this even be here?
Formatted the second stanza (Warrior Ethos) in similar fashion to the way it is primarily displayed.
Added merged ""Warrior Ethos"" copy.
Added a reference to age of previous version.
I do not know what the Washington Post reference references."
"And the guideline is usually interpreted as ""Wikipedia doesn't include spoiler warnings""."
"
Reading the article, it is stated that the Carver does not have any genitalia, however by my understanding of sexual development in humans, this isn't particularly possible. The most common results are a vagina, or penis/testicles, however intersexed conditions only develop in a range of variation between a vagina and penis/testicles. To say that the Carver has no genitalia at all is highly suspicious, what evidence is shown on the show to indicate this? Is there a graphic of Costa demonstrating no genitalia at all, or he simply described as having no genitalia. Likely in the later case, they would mean that he is absent a penis in order to perform the rapes, but not entirely absent of genitalia. (However, this wouldn't be the first time that Nip/Tuck has made errors about intersex/transgender/transsexual people. In the first season there is an episode about transsexuals, although all of the people depicted are cross-dressers, and have a significantly more masculine appearance than most all actual transsexuals.)"
"
::Agreed a source is needed, but this sounds like just the basic view of vajrayana buddhism to me and not particularly controversial aside from the military language. Though it's a little off, because once one realizes the vajra body one also realizes the inseparability of all beings. So not so much that they possess the same essence (that idea of possession implies truly established existence) but more that one realizes buddha nature was all there really was to begin with. Something like that. -"
"Lies, deception, manipulation, cheating, stealing, murdering, are all on the rise in our government based on statistical analysis of the national security archives. Do you know why a headline like this will never be announced? There are many pilers of classification; do you know what they are? Confidential, Secret, Top Secret, Secret Compartmentalized Information, all with differing rules and guidelines for dissemination such as, “need to know” and handling procedures. What is it all about? Why does our government need such a thing? Primarily the classification is proportionate to the severity of the crimes committed by our government. The differing levels of classification act as a buffer hiding the truth.
What if he wall came down tomorrow and all of this information was released to the world? Would there be civil war? Would the leaders be prosecuted? How long will it take for the world to be at war with us? What kinds of secrets are being hidden behind steel doors? The truth about secrets is that a system of checks and balances do not exist. Can collective intelligence truly grow and will humanity ever recover the final battle for freedom and at what lengths will the government go to protect the lies. The protections that our constitution provides will no longer suffice because of the national security loophole."
":::::: Propose the text about PEMFs that you want to restore/add in a new section below, with the sources that support the text, and we can all discus it, thanks. (Or indicate in the section below if you mean the bone healing bit)
"
"* Pagerank 3 = 41553912 sites estimates
"
"
:The problem with saying that the icon collage doesn't have to all inclusive is that the scope of this project is all inclusive. I personally don't see a picture of a sunset as a sun worshiping symbol, but you object to it on those grounds. You're saying that I'm fabricating a controversy (even though if you would read the history of the banner and some of the other activities of this project, you'd see that it's not fabricated at all), and I could say the same of you - that you're fabricating a controversy over an innocent photograph of a landscape. On the other hand, you want folks who would object to seeing a religious icon on their page that they object to, or not see their own icon on their page because their religion isn't deemed important or widespread enough to just accept that the graphic that we pick is good enough. My vote is for the project not to have a graphic at all - graphics are superfluous and certainly not mandatory for a project."
"
Why do i have to stop?I got just the same right as you do.I'm not adding lies,big articles.I am adding one line,that is very important.Not for me,but for people who was working on this case.You don't know the case better than them.If you like to,add something what is better,but stay true to the facts,that they NEVER FOUND any evidence that he isn't the Zodiac.I'm tired of this anti Graysmith thing.He was the first one to write bout Zodiac and you should be thankfull.Greets and wait for my next edit."
"
: Good length, needs a little tidying up and some inline citations."
"
no problem. Enjoy."
"
if you go to the current group section of friends.kalahari-meerkats.com there will be group shots on the right hand side if you scroll down to the vivian and click for a larger view you will see a one eyed dominant standing in front of the researcher that should be all you need to see that the vivian are portrayed as the commandos and then if you look at the monthly reports and the map on the kmp website the whiskers dont encounter the vivian but they do however encounter the commandos who tookover whisker,elveera,and young ones land."
"The parliamentary section has been amended and a bit now reads:
""Two years later, in 1888, he secured passage of a new Oaths Act, which enshrined into law the right of affirmation for members of both Houses, as well as for witnesses in civil and criminal trials.""
I'm almost entirely certain that this is misleading, but I've not changed it because I want to check. There was in fact an earlier act (1869?) allowing atheists to give evidence in court, and it was to that Bradlaugh appealed when first asking to be allowed to affirm (the request denied, of course). His 1888 act may have consolidated all the legislation but it didn't originate the right to affirmation in court, just for parliament. ~~"
"
::::Agreed, I had not thought of that I will be more careful in the future in this regard. '' /"
"
the census reference needs to be fixed."
"
I assure you I am not being paid by anybody to contribute to Wikipedia, I do not support George Bush or Tony Blair and I do not see how Hitler can possible be 'close too centre wing'. Wikipedias definition of far right:
In the modern world, the term far right is applied to those who support authoritarianism, usually involving a dominant class (which may be aristocratic or defined along racial or other lines), and/or an established church . Their favored authoritarian state can be an absolute monarchy, but more often today it is some form of oligarchy or military dictatorship. This is most true in regions and nations that have no real history of monarchy, such as Central America (discounting the Pre-Columbian era), Switzerland, and the United States. The term ""far right"" also embraces extreme nationalism, and will often evoke the ideal of a ""pure"" ideal of the nation, often defined on racial or ""blood"" grounds. They may advocate the expansion or restructuring of existing state borders to achieve this ideal nation, often to the point of embracing expansionary war, racialism, jingoism and imperialism.
Hitler was nationalst, embraced expansionary war, a radical, imperialst, authoritarian and supported an established church all of which are traits of the far right. Bush is nothing more than an idiot and Blair is failing to take tough enough action on the EU, immigration and a whole host of other issues. I will see what i come up as on the Political Spectrum test."
". I hate, for no good reason, dotting that final t and crossing that final i. Cheers,"
"
::::::Whilst I'm aware of this, and the specific series of events that led to your totally ludicrous and pointless block, I'm also of the belief you can't be sure every apple in the barrel is rotten just because the first one, two or three happen to be. Maybe you have to throw out a hundred apples but one at least will be edible, even if not totally palatable. And one might even make some cider."
"Came across this and I figured with your keen interest in B&H; churches (or probably just heritage in general), you might be interest in it."
"
Why on earth isn't there mention of the controversy surrounding Barnes performance in the All Blacks/France and Wales/South African games? For example, there is nothing about ""that"" kick. Who is protecting this page from it? Wayne Barnes'family?"
"You are giving a lone example of aishwarya rai. why not you go and see the wiki pages of sharukh, salman, amir, saif, rekha, hemamalini, sridevi, esha deol, vidya balan etc. The language scripts of their mother tongue and their main career industry is only displayed. If one has to add the language scripts of the all the languages an actor appeared on the screen, then Kamal hasan's wiki page should have atlease eight scripts. so apply common sense and display only their mother tongue and main career industry's. I wish some one add aishwarya's script in Hindi since her main career role is in that industry. without doubt, she was brought up in the film industy by Tamil and she herself told that she can communicate in Tamil. But I don't want to add Tamil script for her coz it's not her career industry.
And finally, to the thread starter, I am not a newbie and ve been around in wiki for about 2 years. - yasirian"
"
::I think that makes a lot of sense. There are now two events and the ""International Birdman"" is not specific to either. Although there is a lot of shared information unless the Worthing Birdman page starts in 2008 linking back to the Bognor one. I am unsure which is the best way to structure these changes."
"Norwegian is placed as a descendant of the Old East Norse, therefore, the Old West Norse should be moved one space to accommodate the Old Norwegian. Also, Old Gutnish should be occupying both the Early Middle ages and the Middle aged spaces."
":As you noticed the lead is the main problem. It's too short. That's the reason why the meaning of the name is crammed in there. Concubine is the wording of Encyclopædia Britannica. Also he was not the first Arsacid to rule Armenia, he was technically the fourth. However he was the first one in a long line of Arsacid rulers. He also didn't establish the Armenian dynasty. That's all mentioned in the lead. Thanks for the source, i'll use it to modify the lead. Please let me know if you come up with anything else. I hope it ends up as an FA as currently there isn't a single Armenian related FA article."
"
:I'm confused. I see mention of Tim in the current version of the article, and from looking at the history it was added on June 22. (You can see all the edits made to an article by clicking the history tab up there at the top of the screen when looking at the article in question.) I assume you (140.198.85.35) are also 167.94.2.9, who added the entry. Possibly you looked at the page prior to the edit on the 140.198.85.35 machine and have an old version in your browser cache? -"
"
Without references, images and additional text this article is only a stub. /\"
"
::Which line are you refering to?"
"
::That last source touches on the point I am getting at: the racing line is not a line through a corner, it is the optimal path around the track. The subtle difference being that while the major influence is the geometry of the corner, the ultimate lap relies on a complete optimisation of all factors."
"
::It would be a good idea and encouraged to add some additional biographical material about the individual himself, but one sentence about this particular issue is appropriate. '"
"
*Remove Qu'ran section. Obviously not applicable to only atheists apostasy is not just about atheism. ""Atheism"" as a self-identifiable group did not exist when the Qu'ran was written and trying to claim it made statements about atheists is not supported by any reliable scholarship on the subject."
"
Ofsted reports are available here ."
"
:I've removed the implication that the main character is a composite character, because he's clearly Avner from Vengeance; if he was a composite character, that's another matter. It would also help to have sources for the controversy paragraph."
"
They were not made of glass, it was hardened plastic. They originally came in the colors of blue, red and green. They were packaged in a plain plastic bag the a fold over label of yellow. I had an original set when they first came out. Another toy that came out at the same time was string-ball. That one I have a picture of."
"
* Done"
"
""The international reactions to the 2006 Israel-Lebanon conflict have been divided, with most leaders condemning both Hezbollah and Israel.""
This first sentence seems a little weird. If most are condemning both, then reactions are not all that much divided, are they?"
"
:::: Thanks a lot Friday."
"
:1/120% = 83.3̅%, I'd say that's close enough. But which one is a derivative of the other?"
"Where did you find that list of the single constituencies? I tried to find it with google some weeks ago without success. And of course I would also love to know the error in my maps, so I can correct it. Fixed the error in Ubon already, was just wrong numbers in the table, on the image description page of the map were the correct numbers."
"
:::: is what I have been working on in the last few minutes. I would be interested to know your opinion...."
"
this article is a ""Featured article"" in 9 different languages can someone make it featured in the all 15 languages?"
"
::::The sole argument for not moving this article is that ""Arabic numerals"" is more common. This is highly arguable and almost impossible to prove. However, there are several arguments for moving it that are documentable and defensible by a logical argument. Now, any neutral party with a mind for logic and fairness could see that the argument for moving it BACK to where it was is the stronger position. Why is the position for not moving is being defended so belligerently? It was moved to it's present location without a clear consensus in the first place! I've run across this several times on Wikipedia; a highly suspect edit becomes entrenched and defended to the teeth on a shaky interpretation of ""policy"". Logic seems to go out the window. That, is absurd. To demonstrate how ludicrous it is to judge something by what is common or commonplace can be, listen to common speech, watch television, you hear incorrect grammar and syntax all the time. Just because it is commonplace does not make it acceptable.
::::I propose the following solution. Move the article back to it's correct title, ""Hindu-Arabic numerals"". Then form a redirect from ""Arabic numerals"" to steer people looking for the subject under the ""common"" term. That is for what redirects were made. Problem solved. The article retains its correct heading that reflects it's relationship to ""Hindu-Arabic numeral systems"" and people looking for it under the vulgar title will still find it."
"
:I don't mean the hammer meat-tenderizer, I mean the tenderizer with the needles."
"=
In 1475, the Swiss invaded the Barony of Vaud and took the castle of Grandson from Jacques de Savoie, the count of Romont and Lord of Vaud. Charles the Bold, an ally of Jacques de Savoie, besieged the castle of Grandson in 1476. The Swiss defenders, believing that they would be spared if they surrendered, surrendered. However, Charles the Bold believed the Swiss threw themselves on his mercy and he executed all 412 Swiss by hanging and drowning. At the Battle of Grandson, a Swiss relief force arriving too late to lift the siege, defeated Charles the Bold and annexed Grandson and its surrounding area to Bern. Grandson was an exclave, surrounded by Neuchâtel and the Barony of Vaud.
"
"
:The problem is with the process."
"
:It looks to have been in London after NYC, although obviously key writing took place in London – I've added something about the controversy when it opened in London (see obscenity allegations para), which is possibly why it was tested/found an open door in NYC first. But this is supposition and it would need more research."
"::I had a feeling after I made it that you might consider the red/blue hard to read. I liked it and I chose a darker red than my original red to further make it easier to read but I had a feeling it might be short-lived. I'd still like to mess with it a little bit and incorporate some blue around the red border, just to incorporate both those colors (the silver, our third color, is not necessary). I know the visual standards manual may want blue on a white background, but I also know that there are people in administration who don't like athletics, associate red and blue with athletics, and therefore want to keep red and blue out of documents associated with the university it's a really political, stupid, stupid thing and FAU won't be able to move forward until they embrace the whole ""owl head, owl country, bleed red and blue"" mentality that other schools with rabid fans (and alumni) have already.
::But that's an aside.
::Good news is that I took some more pictures (FAU sign, multiple apartment angles so we can choose, College of Business, etc) and I'll do my best to put them up tonight. We'll discuss those soon."
"
:Hi , nice job, very nice pages. I probably do have sources for both articles, but will wait until they clear DYK to add anything."
"
—— ''''''"
"
Thanks for sorting out the reference on my addition to the A14 article, I wasn't absolutely sure how to do that. Useful to learn!"
"
This whole thing reads like it was written by the company. Lame lame lame."
"
Hi,
Ronz may have overreacted in his response to your edits, in that you appear to be a new user. Please review some of the links above to help you familiarize yourself with wiki policy and guidelines. Generally speaking articles need to contain text from reliable sources, that is newpapers, magazines, scientific journals, scholarly books etc ... Articles made exclusively from self-promotional sites are not acceptable on wikipedia. That is why the article was deleted. If there was research published in a reliable source on these exercises that it would be encouraged to add links and perhaps create a page for the topic. Generally, the types of mistakes you are making are unacceptable from an experienced user, but totally understandable from a new user and are generally met with a polite welcome and some introductory information to introduced you to editing on wikipedia. Welcome, and we hope you decide to contribute."
":::That would be wrong, as you say. Rather, the edit said that these two equations were equivalent:
"
"
**It's about which side of the river things are on. You can't have a mill pond on the west bank of the river powering a factory on the east bank unless you've got a ton of elevation and an aqueduct across the Schuykill."
"::I think Sin ra is Silla/新羅, an ancient Korean kingdom and Fujiwra no foufira is Fujiwara no Fuhito/藤原不比等. As for Wado, it's famous for the first Japanese coin, 和同開珎(wadokaiho/wadokaichin)."
"
*If you disagree with the assessment, please change it by editing the class parameter of the {{WikiProject California|class=stub|importance=}} above to the appropriate class and removing the stub template from the article."
":All said issues have been addressed. EDIT:''' I dont know what happened. I made all the changes but it doesn't save them. I guess beat me to it. Anyway, if you dont mind, could you look over it again and tell me what could be done to reach FA. I believe this article is truly close. Thanks a bunch."
"
But it does exist, check the ""exists"" part, and there says that the account was created on August 1, 2006."
"
Some questions about this article.
1. The statement that Chilvers invented the Windsurfer is like stating that someone invented the Xerox, or the Kleenex. Just as Xerox and Kleenex are trademarked terms which have come into common use, the term Windsurfer was a trademark of Windsurfing International, Hoyle Schweitzer's company. This is clearly demonstrated in records of the US Patent and Trademark office, which shows the registration date as July 3, 1973 . Courts also recognized that the Chilvers invention was different in design from the Schweitzer Windsurfer . Therefore it would seem more accurate, using a generic term, to state that Chilvers invented the sailboard. If there is any evidence or record that Chilvers named his invention a ""windsurfer,"" prior to 1972 when the Schweitzer trademark registration was filed, this would be relevant.
2. According to the Bicsport website, Tabur was a pre-existing French boat-building company acquired by Baron Bic in the late seventies. It introduced its first boat in 1968 . In light of this, the claim that Chilvers was the founder of Tabur Marine is puzzling. Is there any info available to clear the link between Chilvers and Tabur?"
"""Werewolves can 'imprint' on a certain person once they begin phasing. Jacob describes imprinting to be stronger than love at first sight. This is, according to the legends, very rare, but ... This is about Jacob and all werewolves!
"
":::I strongly advise you not to re-add it. I assure you it is not the way to get your opinions heard. Just post a note saying that you've posted an essay on it at RR. When in Rome, '''
"
"
I added the following text. Some user removed it saying it is nsourced and NN IMO.
I suppose NN stands for Not Needed. I do not see why that would be the case.
An alternative would be to add this in the euro section of the VC page.
TEXT:
In the European Virtual Console some but not all games are available in the languages they were originally published in.
Some games are multilingual (as they were in their original form, example: Super Mario 64), others are avaiable in mutliple versions (example: The Legend Of Zelda: A Link To The Past). The Wii's country setting decides which version will be available for download. If a user wants to download more than one version, he is charged once per version.
An example for a game not available in all of its original language choices is Kirby's Adventure. The NES game was published in a fully localized version in Germany, but only the English version has been made avaiable for users connecting to the Wii Shop Channel, with a German country setting, in time of the game's Virtual Console release."
"
::::Okay, I added the sensical ones that actually have articles in them to the top of the list and put them at . I asked the bot to be run at ."
"
In case, you continue to vandalise pages, you mat be permanently blocked from edition. Please be careful."
"
a)The English master, as he seems to think he is, spelt sentence wrongly. (I don't care if I spell words incorrectly, but it's rich when the person who pulls me up for grammar and spelling cannot spell even the simple word 'sentence' correctly)
b) When I wrote, do your worst, I was not refering to this arguement, but in the real, non-ethernet world.
c)As I stated previously, I have already won this arguement, so how exactly do you think you won??? Explain, if you can (I assume you can't).
d)You call me a child, as I am, but I'm older than you; sure, it may be by a few days- but I'm still older. Ha.
e)Don't patronise one whom's own intelligence outways that of yourself. (I'm refering to the 'simple' remark.)
f)I've kept this arguement clean and inoffensive, you however, have no dignity and lack self restraint in the 'crudeness' department. So now, try and come back. I throw you the guantlet. Prepare for a metaphorical duel."
"
Hello Fainites. If you have anything specific that you wish me to doublecheck, then post it on my talkpage and I will see if I can. But I do believe that with so many anonymous and odd edits going undiscussed on the NLP article, even verbatim quotes will be misplaced. It does seem to me that the anonymous editors are sockpuppets of Comaze. I would rather edit other articles where this problem is not occurring.
Hi again Fainites. Here is the quote in full from Sharpley 87:
The most conclusive sentence (about conclusions) in that section you presented is that “There are conclusive data from the research on NLP, and the conclusion is that the principles and procedures of NLP have failed to be supported by those data”
Then he says “On the other hand, Einspruch and Forman (1985) implied that NLP is far more complex than presumed by researchers, and thus, the data are not true evaluations of NLP. Perhaps this is so, and perhaps NLP procedures are not amenable to research evaluation. This does not necessarily reduce NLP to worthlessness for counseling practice. Rather it puts it in the same category as psychoanalysis, that is, with principles not easily demonstrated in laboratory settings but, nevertheless, strongly supported by clinicians in the field. Not every therapy has to undergo the rigorous testing that is characteristic of the more behavioural approaches to counseling to be of use to the therapeutic community, but failure to produce data that support a particular theory from controlled studies does relegate that theory to questionable status in terms of professional accountability”
Right at the end of the article the sentences read:
“Elich et al referred to NLP as a psychological fad, and they may well have been correct. Certainly research data do not support the rather extreme claims that proponents of NLP have made as to the validity of its principles or the novelty of its procedures.”
I think the only thing to do with this is rely on what the other researchers (eg Devilly, Eisner and so on) say about Sharpley. Also, if NLP is actually mentioned in the Norcross research that AB presented then that may help as it is even more recent regarding acceptance by clinicians."
"
:If you don't care, then don't. WTF?"
"
::::::As I already pronounced, I know very well about the WP conventions. And most of the time, they are very reasonable. But, IMHO, I am not so sure about obvious errors. It can't be the rationale to keep up an obvious error just for the sake of some ""majority"". And since the end of WWII, they changed it from y to i, which is also reflected even in some English sources, as you showed above."
"
What for?"
"
Has apparently died a couple of days ago. —"
"It would be interesting to know who was the youngest player to reach 100 wins, 200 wins, 300 wins, and on. Rafael Nadal is probably the best in most of these records (now is 600 wins with 26 years and 9 month), do anyone know where to find that record?"
"
* There are many disussions about this, and I can surely said that there are more common, more widespread, and more known ornamental symbols in Armenian culture."
"
::Isn't that what page protection is for? —"
"
OMG SHUT UP U STOOPID"
"I think the whole Andromeda Paradox is incredibly misleadingly described. If two people are standing in the street, and one person starts walking away from the other, then he absolutely will not view events in the Andromeda galaxy which are two days apart. That's a nonsense. As an example, let's imagine you are viewing a supernova through your telescope. If you decide to move your telescope to the left by a couple of foot you don't suddenly see the supernova as it existed two days ago - before it exploded. Move your telescope - the supernova explodes; stop moving your telescope - the supernova doesn't explode. Obviously not the case.
:The paradox is about what events observers consider to be on their x-axes, no-one can observe such events, they can only observe things transmitted from the events like light. This does not mean that the events on the x-axes do not happen and, if you use SR to allow for the transmission of light and the effects of relative velocities you get the Andromeda paradox. (ie: Penrose is not stupid!).
If that was that case then the speed of rotation of Earth would dominate any relative velocity difference of two people walking in the street - people on one side of Earth would see the supernova exploding, while people on the other side of Earth would see the supernova two days earlier before it exploded. We'd see total confusion in space! Roger Penrose actually describes it misleadingly in his original description in his book: ""Even with quite slow relative velocities, significant differences in time-ordering will occur for events at great distances"" - well, that's absolutely not the case if the two observers are not spatially separated (see my earlier example about viewing a supernova).
:The paradox is about what events observers consider to be on their x-axes at any instant, not about the order in which photons are received. Penrose is not stupid.
::But the order in which photons are received defines what events the observers consider to be simultaneous. Remember the old special relativity thought experiment of an observer in a moving train sending two light pulses out to the back and the front of the train? He says the rays arrive simultaneously precisely because he sees the photons arrive at the same time (of course, it's not just limited to light - the speed of light provides an upper limit for any information). It's that speed of light which defines that x-axis to which you refer. I know Penrose is not stupid, but this Andromeda Paradox makes the mistake of overestimating the effect of special relativity on two walking observers. As I say, the rotation of the planet would have a far greater effect than any walking velocity! Penrose has made a mistake here.
:::The debate is about the geometrical rather than the dynamical interpretation of SR. Both observers get the light arriving in the right order and the car receives the same light signals as the man on the street while they coincide. The issue is a philosophical enquiry into the nature of the geometrical interpretation of SR - if the world is a (3+1)D manifold then, although the x axes of the man and the car coincide where they meet they diverge with distance by seconds with distance along the man's x axis. The x-axis is on the hyperplane of things that are simultaneously present so, according to the geometrical interpretation, the car has different events on Andromeda in its present moment from those on Andromeda in the man's present moment.
If two people in the street are not spatially separated then they WILL agree about simultaneity (though one might experience time dilation - his clock might be running slower). And if the two observers are not spatially separated AND their relative velocities are small then there will be absolutely NO difference of opinion about simultaneity - in the Andromeda galaxy on anywhere else - OR time dilation. Basically, their experiences will be identical. I think the whole presentation of this ""paradox"" is desperately flawed and is misleading and basically incorrect in its current form as presented here. The only way that small relative velocities would have an impact (as suggested by Roger Penrose) is if the two observers are also separated by a great distance (i.e., the combination of small relative velocity IN COMBINATION with a long time for light to reach the observers from a distant galaxy has resulted in the observers being far apart when the two signals reach them - so they disagree about simultaneity - this agrees with the comments of Pgb23 above: ""In actual fact the car would have to have been travelling for as long as the light signal from Andromeda ... It would be no good briefly increasing one’s speed.""). The Rietdijk-Putnam argument is fascinating and fundamental, but - with the greatest respect for Roger Penrose - the ""Andromeda Paradox"" is just plain misleading in this form and should be rewitten or (preferably) deleted.
"
"
ΎHi"
"
::::Thanks a lot Haemo... im kinda nu to this whole thing so i just had truble undrstanding sum of it S
I guess ill just have to learn things from my mistakes hehe
oh and one lassssssst thing... I promise lol! how do you get the link in html to see anothr users contributions... u know... like how the thing to get to someones talk page is 'user talk:.....whatever'... so what is 'contributions'?"
"
Article says France and the United Kingdom were the only countries to develop fleets of wooden steam screw battleships, although several other navies made use of a mixture of screw battleships and paddle-steamer frigates. These included Russia, Turkey, Sweden, Naples, Prussia, Denmark and Austria.
I don't follow: britain and france were the only countries with wooden steam screw battleships. But then it says several others had screw battleships. Is that screw metal battleships, which seems redundant because lots of people had this? Or screw wood but also paddle and wood, which is worth making as a distinction because some of their ships had paddles? This is a complicated way of saying it seems to say only britain had screw wood ships, but some others had screw wood ships. Britain never had a fleet exclusively screw wood."
"
I could be mistaken, but I seem to recognize Canon in C by Pacherbell in this song, though I have not yet found any confirmation of this. One of my hobbies is working on music, and when I was using the Canon in C, I recognized the melody from Scatman's World. Despite having no confirmation, I am 99.99% sure that the Scatman sampled the Canon (in C) for this track."
"
:Honestly, Will, that's unfair on so many levels. You are trolling again. If I am to assume good faith on your part, I would have to say that I honestly can't believe you actually believe the bigoted and uninformed views of the extraordinarily complex situation you spout here."
"
I really appreciate your contributions to this article and your desire to improve it, and I want to help you toward that end. Would you be willing to visit me at my talk page and drop me a message so we can work on the article in the next few months? Thanks."
"
:: I think this article is one of the better examples of how encyclopedia entries can be encyclopedic yet far more readable and lucid than your average encyclopedia article."
"
A caution for the current wave of ""succession planinng"" panic setting in because government and business is facing the challenge of how many ""baby boomers"" might leave their employment soon. Traditional succession planning has focused on charts with names of potential temporary or permanent successors for a key vacancy. This is good thinking for the most part. However, it
is not as robust an investment as a progressive 21st Century enterprise should be making. ""Leadership succession,"" a more
recent evolution of traditional succession planning, focuses on an enterprise-wide development of leaders at all levels so that
the organization can be confident a ready pool of talent is being readied to compete for positions that might come open.
Rather than focusing only on ""replacement"" (succession) and targeting a limited one or two names, leadership succession invites many to identify their interest in possible moving up and become involved in leadership development to prepare. This process is more inclusive (read fair) and if done right becomes an accelerator for organizational success.
Les Wallace, SignatureResources.com"
"
zc = 1 zm = 8 zs = 1 f = 4 IDAT too big"
"
::: If we could manage to relax a little, we could solve many more disputes than just this one. Hopefully after your latest response, that concern can be put to rest. Incidentally, it's spelled ""Hadad"" in the source; is there some reason for the ""Haddad"" spelling?"
"
:: The sources have been there all along, I think you tagged the article as a candidate for deletion a split-second after I wrote the first draft and before I was able to finish up the second draft which had my sources cited."
"
::That confused me too, someone should consult the laws before changing it I suppose but it does seem that it should be the other way round."
"
Hi. I believe the two fact tags that Peter has added should be citations to the books in Further Reading. Does anyone have access to these books?"
"
{{familytree|border=0 | | | | | s1 | | s4 | | s51 | | s73 | | s84 | s1=1|s4=4|s51=5 (1)|s73=7 (3)|s84=8 (4) }}"
"
Manchester: ManchesterIX"
"
::Several years ago I was bombarded with adds for a diploma mill out of Detroit; I forwarded the information to Gordon Gee, the president of Vanderbilt University in Nashville, Tennessee. Naturally, he was incenced. I suggested that the students from his journalism class do an investigation, much like students in Illinois investigated wrongfully-convicted inmates on death row. The emails soon stopped."
"
*Well, didn't I add extra data myself yesterday? Please don't act as if just because you wrote it, nobody else can add. I also have PB5 myself. You're acting like that other PB author."
"
::::I think Buckshot06 must be right on this. Some of the industries represented build white goods, scientific instrumentation and other stuff. So, if the Space Station ever needs a new microwave, these guys can organise the item and the launch vehicle ;o) ♠♥♦♣"
"
Okay. I'm surprised that you don't have an opinion on at least the first, third and fourth points mentioned just above, but if you need time to look through the discussions, that's cool. Thanks again. -)"
"
:: Thanks, I missed it. I'll look again."
":The help desk is not for others to do your homework, either. /"
"
Thank you very much. You helped me a lot. ^^"
"Please do not save test edits. If you want to experiment, please use the sandbox."
"
::Of course, will do. Thanks very much for your advice too. I never thought about templates - so obvious as well!"
"
Kuantan shall be a big place. Its just not the city. It is around 1200 square km huge. More photos should be placed. Take a shot on the scenery especially at the Panorama hills, the city, the port, ant the waterfall please."
"
:Oh no, completely opposed to such a solution."
"
What family is Fruitadens haagorum? If there is, it should be the lightest."
"
*** I didn't see any reference to ""Japs clan"" anywhere in that image."
":Have you perhaps mixed up the Freedom House ""Freedom of the Press"" report with their ""Freedom in the World"" series, which is the subject of this article? Checking their website, Venezuela is indeed show ""Not Free"" in the ""Freedom of the Press"" study, but this is not the same as the ""Partly Free"" rating it is given in the most recent ""Freedom in the World"", which is what is being listed here. The first would be looking at press freedom specifically, the second at the country overall. I'll revert your change - if I'm mistaken let me know. - You are not mistaken.
"
":::Okay! '
"
"
Looks like I'll need to refer to those resources a few more times. The one thing that jumped out at me (at the moment), is the majority/minority view discussion. In the current section, I would interpret that the conflict with milbloggers has had a major impact on the perception of his credibility and hence his readership. The milbloggers had previous helped build his prestige and it was the conflict with them that then diminished it. Conversely, he does still maintain very loyal fans. Looking at it from the outside in, it would appear to me that the views are probably equally divided and ardently argued by the two sides, with few in the middle. In short, he has become a very polarizing figure in the period since he took on BG Menard.
All of that to say, I'll be digesting the references you gave to properly moving the article forward. It will be a couple of days. Thanks again.( )"
"This page is software-specific. Is that right?
"
"This article has been expanded - please list below any points of disagreement so it can be edited in a sensible manner.
There is too much criticsm, i want to know more about the actual group
Those from the Quilliam Foundation (ie ed husain and majid nawaz) or pro-Quilliam who dislike critique, please stop vandalising the article.
"
"
:::Considering he's going through the effort to come up with a new sock almost every day, it could be merited."
"
:And i intended to add to it, rather than leave it at the two or three sentences i first wrote, but got called away, so it was a pleasant surprise to log on and see what superbe expansion you wrought. Cheers, ''''''"
"
Through the article both names, Les and Stroud, are used in an interchangeable. I think it would be better for consistency to either use Les or Stroud on all the article and replace all occurrences of the other name."
"
Someone made a change to the page (which I have reverted) stating that Moe is a city. Moe actually has a population of about 17,000 which makes it quite small in the scheme of things. If you have evidence, such as government documentation etc., that states that Moe is a city rather than town, please link it in the discussion group/add it as a reference prior to editing the page. If you do have a credible reference and can show then please change it back to city, with my apologies.
That said, the only alternative to 'town' that seems like it would be appropriate is 'suburb', as in 'Moe is a suburb within the Latrobe Valley/Latrobe City (which includes towns such as Traralgon, Moe, Morwell, Churchill, Yallourn North, Newborough, Hernes Oak, Hazelwood, Tyers, Westbury, etc). The term 'Latrobe City', is actually the title of the shire."
"
how can it be vandalism when what I wrote is true?"
"
::::I didn't catch the comments made by the anon poster about Columbia before I hit save. I tend to agree that Columbia University would probably be a better fit for WPNYC since it is based in Manhattan. That said, if WPNYC doesn't want to take the project on board, WPNY will add it at some point. – ''''''"
"
:Where is there Romance in One Piece?"
"
Why are you accusing me of sock puppetry? Do you really think I'd be capable of creating another account to remove the R&B; genre from Justin Timberlake's article? This is ridiculous, I'm not afraid of showing my face and assume the responsibilities of my acts. But I wonder what Justin did to be an R&B; artist."
"
What year did you graduate New World?"
"
:Oh, I was unaware of that. Thank you for pointing that out and changing the article to reflect such! Best,"
"::Your link was labelled other definitions uses, which it wasn't, it was the same definition. The 'joke' isn't explained, and so may confuse. Wikipedia isn't a joke book.
"
"
::::Thanks; I should have looked a bit closer in the first instance. )"
"
:: point taken on skyline photos, but i never really said it symbolises greatness... just something recognisable about the city. those photos generally contain the most recognisable landmarks of those cities along with the (recognisable also) skyline: sydney harbour and opera house, the yarra, the story bridge over the brisbane river, swan river with its sandy shores and an actual swan. and the festival centre is in the picture of adelaide."
"
3(a) Minimal usage. As few non-free content uses as possible are included in each article and in Wikipedia as a whole. Multiple items are not used if one will suffice; one is used only if necessary.
There is no real need for all 3 covers. As stated the really don't add anything to the article. This is not free content and as such 1 cover is enough to illustrate the series."
"
Hara! D
( )"
"
It needs to be mentioned, if only in passing, that these figures are for COMBUSTION, in air (I assume)"
"
Be careful man. There is no such thing as if past midnight you're entitled to 3 more rv's! It applies for any 3 rv's within a 24h period (regardless dates). I'll help in the discussion if you wish, start by citing different definitions of European borders..."
"
:I've noticed that my vote for deletion was altered on the AfD for Yetol. The original text was Totally Non-notable which was changed to Somewhat Non-notable'. It is not acceptable to alter other people's comments in an AfD and doing so could get your account locked."
"
::Frankly, this is b.s. For such a ""well-documented"" phenomenon, the article certainly is lacking in references backing up such a claim."
"
Right then, I'd like to start with some boring little bibliographical questions about the article on Štreit. (For which I thank you. I don't know his work, but a good rule of thumb is that anything exhibited by Amber/Side is worthwhile. I'll investigate.) The questions are very simple, really, but unfortunately in order to ask them I have to use a lot of bytes.
Worldcat tells me of Josef Moucha, Helena Musilová, and Eva Marlene Hodek, Fotogenie identity: pamět̕ české fotografie / The photogeny of identity: The memory of Czech photography (Prague: Pražský dům fotografie, 2006; ISBN 8086970124).
(It may be better to say that Moucha, Musilová, and Hodek are the editors, or that all are contributors but that Moucha is the editor. I can't read Czech so I'm guessing.)
Surely ""Prague House of Photography"" is the name in English of Pražský dům fotografie; let's not quibble about which to use. And perhaps Kant is publishing it on behalf of Pdf, just as (for example) Yale University Press publishes books on behalf of the photo museum at Houston.
However, the article seems to imply that this book is ""in: Dufek, Antonín: Vesnice je svět (The Village is a Global World), Prague, Arcadia 2003. Preface.""
I guess you mean that what Dufek is quoted as saying is from Dufek's preface to his own Vesnice je svět, via Fotogenie identity: pamět̕ české fotografie. Is this right? If so, perhaps:
:. . . ; quoting Antonín Dufek's preface to his Vesnice je svět (The Village is a Global World; Prague, Arcadia 2003; ISBN _______)
or of course the other way around:
:Antonín Dufek, preface to his Vesnice je svět (The Village is a Global World; Prague, Arcadia 2003; ISBN _______); quoted in . . .
I looked in WorldCat for this book but couldn't find it. However, I was able to find this quadrilingual book by Štreit (photos) and Dufek (text): Vesnice je svět / The Village is a Global World / Das Dorf ist eine globale Welt / Un village, c'est tout un monde (Prague: Arcadia, 1993; ISBN 8090142354). Perhaps the 2003 book is a new edition or just a reprinting of this.
In the list of references, why ""coll.""? Is this something like ""collective authorship""? If so, sorry but I don't think that this abbreviation is commonly used or much understood in English. (If a book has many authors, of course it's fine to name the first one and say ""et al."" for the rest.)
Excuse the boringness of my questions. More importantly, I'm glad that you wrote an article on Štreit and I look forward to looking at his work. (More on Tichý and others a bit later.)"
"
::Agree with above user. King Pharmaceuticals is a separate article and should be represented by a link, not by a substansial repetition of key portions of the article here."
"
I rewrote the article remove the promotional text and better reflect the magazine. As a result, I humbly submit that we don't need the News Release flag, so I removed that too."
"
::::Have they been involved in any high profile activites since 2006?"
"
:You have chosen not to respond to my very simple question, which I have now copied to my talk page. Please respond ."
"
This article would benefit from additional content, including a photo."
"
::Well isn't it worth mentioning that Matt turned face again?"
"
Congrats on your successful admin request. May god be with you on Christmas"
"
:If you want to talk to me, please post in the talk page, not in the user page."
"
I think it makes perfect sense to point out that Jersey is not ranked because some other non-independent countries are. However Jerriais rugby, like that of the Isle of Man, seems to have subsumed itself in the RFU, despite Jersey not being part of England."
"
Is it a bad sign if you edit the Wiki during a layover in PHX using their free wireless?"
"
I have renamed this article as mentioned on the project page."
"
:No problem."
"
out of curiosity whats the difference between most famous and most popular
""The most famous meganekko in current American fandom is probably Yomiko Readman of the Read or Die (ROD) series. However, in the 1990s, one of the most popular was arguably Tira Misu from the series Sorcerer Hunters, which was an early manga import to the US. The comedic anime series G-On Riders (a pun that also means ""glasses-on"") has character designs deliberately incorporating this design.""
also what statistics does this come from?"
"
Thanks for your message; I hope that my approach is successful — don't let's count our chickens yet...."
"
:When I looked at the word 熟冷(숙랭) in dictionary, it says it is just a cold water served for ancestor worship ceremony. I think there might be relationship with the word SungNyung(숭늉) but the dictionary doesn't have any detail about the relationship."
"
Just noticed ""unique ethnic identity has been harshly repressed"" while skiming the article. I am sure there are other examples of non-neutral content.
I do not see how ""Turkey's first female pilot and the adopted daughter of Atatürk, took part in the bombing raids against the Dersim Kurds"" is relevant to topic either..."
"
:No, we are an encyclopaedia, not a ""terrorist catching group"" - we have no interest in what you do if you think you see someone that looks like him."
"
:::Well, almost certainly the problem is with the SVG image itself or with something else Commons-related. The templates used here are very stable, and work with thousands of flags, so I am certain there is no problem with the template code."
"
('''''''''')"
"I like the part about the ""UltraOrthodox Jews that the wider Jewish community considers anti-Semitic""! It appears that everyone, even Orthodox Jews, are guilty of anti-semitism. Are these Jews considered anti-semitic simply because they are anti-Zionist? If so, this must be one of the most glaring examples of misuse of the phrase ""anti-semitism"" for political purposes and it calls into question whether the allegations of anti-semitism made against Arabs are due to the same blind spot."
"
See . Exactly what is wrong with reference style. Full details are given. What do you simply delete sourced material? The intro mostly have anti-nuclear arguments. In order to achieve NPOV there should also be opposing views in the intro."
"
Is not purple the color of royality as opposed to blue?"
"
I wrote a very precise sourced version and Captain Occam insisted it be shovelled away into a non-existent criticism section. The point is not about Lynn. but whether the data presented is up to snuff."
"
Hutaree is NOT a fanclub, it is a GANG, better yet, A cult that is trying to hide behind the fanclub name, the FBI person in charge of the case even said this is not a fanclub group, other fanclub groups also reported this gang to the FBI, since most fanclub groups have active, and leo police officers as members, they use the fanclub as a way to buy guns from other groups, but the other groups are aware of these type of people and always ignore them or turn them away, like they did with Hutaree.
Hutaree is NOT a fanclub, it is a GANG, better yet, A cult that is trying to hide behind the fanclub name, the FBI person in charge of the case even said this is not a fanclub group, other fanclub groups also reported this gang to the FBI, since most fanclub groups have active, and leo police officers as members, they use the fanclub as a way to buy guns from other groups, but the other groups are aware of these type of people and always ignore them or turn them away, like they did with Hutaree."
"
If the C-27J wins the US JCA competition, I plan on splitting the C-27J models off to their own page. If not, I don't forsee a problem leaving them here, as there is not really enough content otherwise. I will try to add some specs on the J in the next few days. -"
"
''''''"
"
The page which i found last year is more authentic than this page."
"
::There's nothing hard to understand with Rihk's comment, he wants to know if we should mark a section in Matt's article called weakness and list his inability to narrow his field of telepathy in a crowded room.
::However, I believe that this is the show's way of portraying his development of his power. Imagine you're in a room full of people, who are all shouting at you at the top of their lungs... you try to listen to only one as you walk around the room, but doing so alters the volume of each person. Soon enough, you'd be likely to have a head-splitter too."
"
:There is no valid reason to do so. He is not making personal attacks nor is he making any extortion."
"
Possible problems in some articles do not mean that problems should remain in other articles. The best is to source according to policy, there are plenty of sources on this topic. Regards."
"
I don't know Montenegrin, but shouldn't ""Narodna stranka"" be translated as ""National Party""?"
"
:You're going to need multiple sources to back up a claim like that. And I'm pretty sure that your theory isn't true anyway. —"
"
::okay will do"
"
Whoever here keeps puting Let go's sales to 13.1 million is absolutely wrong!!! Media Traffic does not cover sales from all countries so there is always an additional 10% to the sales that it claims. (i didnt make that up, media traffic has said it!!!!check it out). Now think about it 13.1 milion + 10% = 14.5 million + sales through years (6 years have passed since its release), 1.5 million (at least)= 16 million +++++++. Plus: Her official website, her record company and all media say the same thing!!!!!!!! SO STOP IT!!!"
"
::::The wider problem with the name is that it sounds official. While some bot names should sound official, it is an interesting question whether this one should. I will raise this elsewhere."
"
Speaking about ""running counter a reliable source"" EA told us her family died in a fire. So, she can't be Jan Fritzges daughter or Sunshynes sister, cause they're still alive. There must be another Emilie Autumn Liddell in Costa Mesa, California, beside ""Emilie Autumn"" (the one from JLs facebook), ""Emily A. Fritzges"" and ""Autumn"" (daughter of Jan Fritzges). Emilie, Autumn and Fritzges seem to be very common names in CA ;)"
"
::::::::::::::I'm ""afraid of tackling the big-boys""? Good grief, you really don't know me. Check my wiki-history.
::::::::::::::So, what are you suggesting. 1) That we should never block anyone for incivility or personal attacks, no matter how bad. (That's a legitimate position, by the way). 2) That we should somehow codify what does or does not deserve a block. (That's impossible, by the way). or 3) We should have some sort of bureaucratic fuck-fest to determine when we block. (That would be fun to watch, by the way). or 4) We should requite admins to block anyone who says ""shity boo""? You may well successfully pour so much shit on me that I never block anyone"
"
I don't know"
"
:I think you're right. Although it looks as though this article only documents states that were swallowed up rather than ones which fell apart."
"
I believe this user is using sock puppets to vandalise other users User pages."
"
: I don't know if that is what the editor who wrote those lines had in mind, but that would be correct, yes."
":Indeed, ""Nationality"" is generally used to denote citizenship, as opposed to what it means in the census. That quote even has the word ""etnii"" in it, in proper context. This should definitely clear up any remaining doubts. Thanks!
"
Well I was impressed anyway. ;-) Less impressed though by yet another demonstration that administrators (Tom) are not held to the same standards they so frequently demand of their underlings.
"
Thank You for answering my questions. I really appreciate it. I am still not really sure why ISKCON must be considered Hindu, yet ISKCON views cannot be considered in Hindu articles. Seems kind of hypocritical. Another thing is that Prabhuphada's Bhagavad-Gita is the most widely read and also He states it is the only pure translation. But anyways, if Krishna Conciousness differs from Hinduism, why can't I create articles like Krishna Conscious Cosmology, or Krishna in Krishna Consciousness, or Rama in Krishna Consciousness ect. I have tried doing this in the past because even though Wikipedia considers ISKCON Hindu they do not allow ISKCON's views in Hindu articles. I find this hypocritical but I gave up trying to fight it so I made my own articles like Krishna in Krishna Conciousness, or Rama in Krishna Consciousness so I could just present the Krishna Conscious view. I cited many reliable Krishna Conscious sources on the Krishna Conscious view but my articles were deleted over and over until they banned me. I don't understand why I got banned. Isn't it fair that if other religions can have their own views on things like Cosmology, such as Biblical Cosmology, Hindu Cosmology, Buddhist Cosmology, and Jain Cosmology that Krishna Consciousness also have a page Krishna Conscious Cosmology to express its unique view if it is not allowed on Hindu Cosmology? Another thing about the Naraka (Hinduism) is that I know there is a Naraka page, but a few days ago I saw the Naraka (Buddhism) page and I liked it and from reading the Shrimad-Bhagavatam it seemed sort of familiar to me. I liked how it talked about all the different Hells one by one giving good description, so I went to see if Hinduism had something like that which talked about all the diferent Hellish planets like Buddhism. but an article did not exist so I spent a few hours to make one. If this is not okay because ISKCON has ""Non-Hindu"" beleifs can I make an article entitled Naraka (Krishna Consciousness) so that the beliefs of my religion can be expressed like the beliefs of all the other religions? Sorry for asking so many questions. Thank You very much for answering all of my questions and for your cooperation, time, and effort."
"
Layer 7 is nog used in Profibus DP and PA with the DPV-0 to DPV-2. Layer 7 is only used in Profibus FMS in a special FMS layer. view official literature The New rapid way to Profibus DP (by www.profibus.com)"
"
HI I'M AN AUSSIE COON BOONG UNBAN KTHX U FUCKIN JEWISH NAZI CUNTS"
================================================
FILE: packages/backend-api/package.json
================================================
{
"name": "@conversationai/moderator-backend-api",
"description": "API Endpoints for OSMod",
"main": "src/server.ts",
"version": "1.1.0",
"scripts": {
"build": "npm run compile",
"compile": "rm -rf dist && ../../node_modules/.bin/tsc --sourceMap --outDir dist --declaration",
"test": "npm run mocha",
"mocha": "WORKER_RUN_IMMEDIATELY=true ts-mocha 'src/test/**/*.spec.ts' --timeout 200000",
"lint": "find src -name '*.ts' | xargs ../../node_modules/.bin/tslint -c ../../tslint.json"
},
"license": "Apache-2.0",
"dependencies": {
"@conversationai/moderator-frontend-web": "1.1.0",
"@types/bluebird": "^3.5.33",
"@types/body-parser": "^1.19.0",
"@types/chai": "^4.2.16",
"@types/chai-http": "^4.2.0",
"@types/compression": "^1.7.0",
"@types/convict": "^6.0.1",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.11",
"@types/express-ws": "^3.0.0",
"@types/faker": "^5.5.1",
"@types/helmet": "^4.0.0",
"@types/jsonwebtoken": "^8.5.1",
"@types/kue": "^0.11.13",
"@types/lodash": "^4.14.168",
"@types/mocha": "^8.2.2",
"@types/node": "14.14.41",
"@types/opentype.js": "^1.3.1",
"@types/passport": "^1.0.6",
"@types/passport-google-oauth2": "^0.1.3",
"@types/passport-jwt": "^3.0.5",
"@types/qs": "6.9.6",
"@types/randomstring": "^1.1.6",
"@types/redis": "^2.8.28",
"@types/request": "^2.48.5",
"@types/underscore.string": "^0.0.38",
"@types/validator": "^13.1.3",
"@types/yargs": "^16.0.1",
"bluebird": "^3.7.2",
"body-parser": "^1.19.0",
"canvas": "^2.7.0",
"chai": "^4.3.4",
"chai-http": "^4.3.0",
"compression": "^1.7.4",
"convict": "6.0.1",
"cors": "2.8.5",
"csv-parse": "^4.15.3",
"express": "^4.17.1",
"express-winston": "^4.1.0",
"express-ws": "^4.0.0",
"faker": "^5.5.3",
"googleapis": "^71.0.0",
"he": "^1.2.0",
"helmet": "^4.4.1",
"joi": "17.4.0",
"jsonwebtoken": "^8.5.1",
"kue": "^0.11.6",
"lodash": "^4.17.21",
"mocha": "^8.3.2",
"moment": "^2.29.1",
"mysql2": "^2.2.5",
"opentype.js": "^1.3.3",
"passport": "^0.4.1",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.0",
"qs": "^6.10.1",
"randomstring": "^1.1.5",
"redis": "^3.1.0",
"request": "^2.88.2",
"sequelize": "^6.6.2",
"sequelize-cli": "^6.2.0",
"striptags": "^3.1.1",
"ts-mocha": "8.0.0",
"ts-node-dev": "1.1.6",
"underscore.string": "^3.3.5",
"winston": "^3.3.3",
"yargs": "^16.2.0"
}
}
================================================
FILE: packages/backend-api/src/actions/assignment_updaters.ts
================================================
/*
Copyright 2021 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {Op} from 'sequelize';
import {Article, ModeratorAssignment, UserCategoryAssignment} from '../models';
import {sendNotification} from '../notification_router';
function getUserCategoryAssignment(userIds: Array, categoryId: number) {
return userIds.map((id) => {
return {
userId: id,
categoryId,
};
});
}
function getArticleAssignmentArray(userIds: Array, articleIdsInCategory: Array) {
return articleIdsInCategory.reduce((sum: Array<{articleId: number; userId: number; }>, articleId) => {
return sum.concat(userIds.map((userId) => {
return {
articleId,
userId,
};
}));
}, []);
}
async function removeArticleAssignments(userIds: Array, articleIds: Array) {
await ModeratorAssignment.destroy({
where: {
userId: {
[Op.in]: userIds,
},
articleId: {
[Op.in]: articleIds,
},
},
});
}
export async function updateCategoryAssignments(categoryId: number, userIds: Array) {
const articlesInCategory: Array = await Article.findAll({
where: { categoryId },
});
const articleIdsInCategory = articlesInCategory.map((article) => article.id);
// Get assignments for the category
const assignmentsForCategory = await UserCategoryAssignment.findAll({
where: { categoryId },
});
const userIdsToBeRemoved = assignmentsForCategory.reduce((prev: Array, current: UserCategoryAssignment): Array => {
const assignmentUserId: number = current.userId;
const isInAssignment = userIds.some((userId) => (userId === assignmentUserId));
if (isInAssignment) {
return prev;
} else {
return prev.concat(assignmentUserId);
}
}, []);
if (userIdsToBeRemoved.length > 0) {
await removeArticleAssignments(userIdsToBeRemoved, articleIdsInCategory);
}
const newUserIds = userIds.filter((userId) => {
return !assignmentsForCategory.some(
(assignment: any) => assignment.userId === userId && assignment.categoryId === categoryId,
);
});
// If a user is being assigned we need to clear and then add them to each article with categoryId of categoryId
await removeArticleAssignments(newUserIds, articleIdsInCategory);
await ModeratorAssignment.bulkCreate(getArticleAssignmentArray(newUserIds, articleIdsInCategory));
// Now remove/set UserCategoryAssignment
if (userIdsToBeRemoved.length > 0) {
await UserCategoryAssignment.destroy({
where: {
userId: {
[Op.in]: userIdsToBeRemoved,
},
},
});
}
await UserCategoryAssignment.bulkCreate(getUserCategoryAssignment(newUserIds, categoryId));
await sendNotification('category', 'modify', categoryId);
for (const articleId of articleIdsInCategory) {
await sendNotification('article', 'modify', articleId);
}
}
export async function updateArticleAssignments(articleId: number, userIds: Set) {
// Get assignments for the category
const assignments = await ModeratorAssignment.findAll({
where: { articleId },
});
const toRemove = new Array();
for (const a of assignments) {
const id = a.userId;
if (userIds.has(id)) {
userIds.delete(id);
}
else {
toRemove.push(a.id);
}
}
await ModeratorAssignment.bulkCreate(getArticleAssignmentArray(Array.from(userIds), [articleId]));
await ModeratorAssignment.destroy({where: {id: {[Op.in]: toRemove }}});
await sendNotification('article', 'modify', articleId);
}
================================================
FILE: packages/backend-api/src/actions/object_updaters.ts
================================================
/*
Copyright 2021 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {ModerationRule, MODERATION_RULE_ACTION_TYPES_SET, Preselect, Tag, TaggingSensitivity} from '../models';
import {sendNotification} from '../notification_router';
export type ModelType = 'moderation_rule' | 'preselect' | 'tagging_sensitivity';
export function checkModelType(type: string): type is ModelType {
switch (type) {
case 'moderation_rule':
case 'preselect':
case 'tagging_sensitivity':
return true;
}
return false;
}
export async function processRangeData(
type: ModelType,
data: {[key: string]: string | number | boolean | null},
setValue: (key: string, value: string | number | boolean | null) => void,
): Promise {
for (const k of ['tagId', 'categoryId']) {
if (k in data) {
let val: number | null;
if (data[k] === null) {
if (k === 'tagId' && type === 'moderation_rule') {
return 'tagId must be set.';
}
val = null;
} else {
val = parseInt(data[k] as string, 10);
if (isNaN(val)) {
return `Invalid value ${data[k]} for field ${k}.`;
}
}
setValue(k, val);
}
}
for (const k of ['lowerThreshold', 'upperThreshold']) {
if (k in data) {
const val = parseFloat(data[k] as string);
if (isNaN(val) || val < 0 || val > 1) {
return`Range error: ${k} is not a valid number: ${data[k]}.`;
}
setValue(k, val);
}
}
if (type === 'moderation_rule') {
if ('action' in data) {
const action = data.action;
if (!MODERATION_RULE_ACTION_TYPES_SET.has(action as string)) {
return `Unknown action: ${action}.`;
}
setValue('action', action);
}
}
return null;
}
export async function createRangeObject(
type: ModelType,
data: {[key: string]: string | number | boolean | null},
): Promise {
const modelData: {[key: string]: string | number | boolean | null } = {};
const msg = await processRangeData(type, data, (key, value) => modelData[key] = value);
if (msg) {
return msg;
}
let mandatory_attributes = ['lowerThreshold', 'upperThreshold'];
if (type === 'moderation_rule') {
mandatory_attributes = [...mandatory_attributes, 'tagId', 'action'];
}
for (const k of mandatory_attributes) {
if (!(k in modelData)) {
return `Missing mandatory attribute: ${k}.`;
}
}
switch (type) {
case 'moderation_rule':
await ModerationRule.create(data as any);
break;
case 'preselect':
await Preselect.create(data as any);
break;
case 'tagging_sensitivity':
await TaggingSensitivity.create(data as any);
break;
}
sendNotification('global');
return null;
}
export async function modifyRangeObject(
type: ModelType,
id: number,
data: {[key: string]: string | number | boolean | null},
): Promise {
let object: ModerationRule | Preselect | TaggingSensitivity | null;
switch (type) {
case 'moderation_rule':
object = await ModerationRule.findByPk(id);
break;
case 'preselect':
object = await Preselect.findByPk(id);
break;
case 'tagging_sensitivity':
object = await TaggingSensitivity.findByPk(id);
break;
}
if (!object) {
return 'Not found';
}
const msg = await processRangeData(type, data, (key, value) => object!.set(key as any, value as any));
if (msg) {
return msg;
}
await object.save();
sendNotification('global');
return null;
}
export async function deleteRangeObject(type: ModelType | 'tag', objectId: number ) {
switch (type) {
case 'moderation_rule':
await ModerationRule.destroy({where: {id: objectId}});
break;
case 'preselect':
await Preselect.destroy({where: {id: objectId}});
break;
case 'tagging_sensitivity':
await TaggingSensitivity.destroy({where: {id: objectId}});
break;
case 'tag':
await Tag.destroy({where: {id: objectId}});
break;
}
sendNotification('global');
}
export async function createTagObject(
data: {[key: string]: string | number | boolean | null},
): Promise {
for (const k of ['color', 'key', 'label']) {
if (typeof data[k] !== 'string') {
return `Tag creation error: Missing/invalid attribute ${k}.`;
}
}
const {color, description, key, label, isInBatchView, inSummaryScore, isTaggable} = data;
await Tag.create({
color, description, key, label,
isInBatchView: !!isInBatchView,
inSummaryScore: !!inSummaryScore,
isTaggable: !!isTaggable,
});
sendNotification('global');
return null;
}
export async function modifyTagObject(
id: number,
data: {[key: string]: string | number | boolean | null},
): Promise {
const tag = await Tag.findByPk(id);
if (!tag) {
return 'Not found';
}
for (const k of ['color', 'key', 'label', 'description']) {
if (k in data) {
if (typeof data[k] !== 'string' && (k !== 'description' || data[k] !== null)) {
return `Tag modification error: Invalid attribute ${k}.`;
}
tag.set(k as 'color' | 'key' | 'label' | 'description', data[k] as string);
}
}
for (const k of ['isInBatchView', 'inSummaryScore', 'isTaggable']) {
if (k in data) {
tag.set(k as 'isInBatchView' | 'inSummaryScore' | 'isTaggable', !!data[k]);
}
}
await tag.save();
sendNotification('global');
return null;
}
================================================
FILE: packages/backend-api/src/api/assistant/index.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import { logger } from '../../logger';
import { CommentScoreRequest } from '../../models';
import { IScoreData } from '../../pipeline/shim';
import {
enqueueProcessMachineScoreTask,
} from '../../processing';
import { REPLY_SUCCESS } from '../constants';
import { onlyServices } from '../util/permissions';
import { validateRequest } from '../util/validation';
import { scoreSchema } from './schema';
export function createAssistant(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
// Return for score information via Pipeline
router.post('/scores/:id',
validateRequest(scoreSchema),
async (req, res, next) => {
const { runImmediately, scores, summaryScores } = req.body;
const { id } = req.params;
logger.info('Process score data to worker for score request ID ', id, 'Body', req.body);
const scoreData: IScoreData = {
scores,
summaryScores,
};
// Obtain information about the score request by ID
const scoreRequest = await CommentScoreRequest.findByPk(id);
if (scoreRequest) {
await enqueueProcessMachineScoreTask(
scoreRequest.commentId!,
scoreRequest.userId!,
scoreData,
runImmediately);
res.json(REPLY_SUCCESS);
next();
} else {
logger.error(`Score request not found for provided id: ${id}`);
res.status(400).json({
status: 'error',
errors: 'Score request not found by provided scoreRequestId',
});
return;
}
},
);
return router;
}
export function createAssistantRouter(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.use('*', onlyServices);
router.use('/', createAssistant());
return router;
}
================================================
FILE: packages/backend-api/src/api/assistant/schema.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Joi from 'joi';
export const scoreItemSchema = Joi.object({
score: Joi.number().required(),
begin: Joi.number().optional(),
end: Joi.number().optional(),
});
export const scoreDataSchema = Joi.object().pattern(
/^[A-Z_]+$/,
Joi.array().items(scoreItemSchema),
).required();
export const summaryScoreDataSchema = Joi.object().pattern(
/^[A-Z_]+$/,
Joi.number().required(),
).required();
export const scoreSchema = Joi.object({
runImmediately: Joi.boolean().optional(),
scores: scoreDataSchema.required(),
summaryScores: summaryScoreDataSchema.required(),
});
================================================
FILE: packages/backend-api/src/api/constants.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// We send this back when we want to send back a success response, but don't need to send back any data.
export const REPLY_SUCCESS_VALUE = 'success';
export const REPLY_SUCCESS = { status: REPLY_SUCCESS_VALUE};
================================================
FILE: packages/backend-api/src/api/router.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import { createAuthConfigRouter } from '../auth/router';
import { createAssistantRouter } from './assistant';
import { createServicesRouter } from './services';
export function createApiRouter(authenticator: any) {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
if (authenticator) {
// Require tokens for our CRUD endpoints.
// Not necessary for `options` requests.
['get', 'post', 'patch', 'delete'].forEach((method) => {
(router as any)[method]('*', authenticator);
});
}
router.use('/', createAuthConfigRouter());
// The services API provides custom endpoints for our clients which would
// normally be awkward REST queries or are unrelated to database models.
router.use('/services', createServicesRouter());
// The assistant API provides callbacks for assistant users to send per-comment
// scores into OSMOD. These are often, but not always, the result of a scoring
// request when a new comment is added by the publisher.
router.use('/assistant', createAssistantRouter());
return router;
}
================================================
FILE: packages/backend-api/src/api/services/assignments.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import {updateArticleAssignments, updateCategoryAssignments} from '../../actions/assignment_updaters';
import {
Article,
User,
} from '../../models';
import {REPLY_SUCCESS} from '../constants';
export async function countAssignments(user: User) {
const articles: Array = await user.getAssignedArticles();
return articles.reduce((sum, a) => sum + a.unmoderatedCount, 0);
}
export function createAssignmentsService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
// POST to category/id who's body.data contains userId[]
router.post('/categories/:id', async (req, res) => {
const categoryId = parseInt(req.params.id, 10);
const userIds: Array = req.body.data.map((s: any) => parseInt(s, 10));
await updateCategoryAssignments(categoryId, userIds);
res.json(REPLY_SUCCESS);
});
// POST to articles/id who's body.data contains userId[]
router.post('/article/:id', async (req, res) => {
const articleId = parseInt(req.params.id, 10);
const userIds: Set = new Set(req.body.data.map((s: any) => parseInt(s, 10)));
await updateArticleAssignments(articleId, userIds);
res.json(REPLY_SUCCESS);
});
return router;
}
================================================
FILE: packages/backend-api/src/api/services/authorCounts.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Bluebird from 'bluebird';
import * as express from 'express';
import * as Joi from 'joi';
import { Comment } from '../../models';
import { validateAndSendResponse, validateRequest } from '../util/validation';
const validateInput = validateRequest(Joi.object({
data: Joi.alternatives().try(
Joi.array().items(Joi.string()),
Joi.string(),
).required(),
}));
const validateOutputAndSendResponse = validateAndSendResponse(
Joi.object({
arg: Joi.string(),
value: Joi.object({
approvedCount: Joi.number().required(),
rejectedCount: Joi.number().required(),
}),
}).unknown().required(),
);
export interface IAuthorCounts {
approvedCount: number;
rejectedCount: number;
}
export async function getAuthorCounts(authorSourceId: string): Promise {
const approvedCount = await Comment.count({ where: { authorSourceId, isAccepted: true } });
const rejectedCount = await Comment.count({ where: { authorSourceId, isAccepted: false } });
return {
approvedCount,
rejectedCount,
};
}
export function createAuthorCountsService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.post(
'/',
validateInput,
async ({ body }, res, next) => {
const dataArray = Array.isArray(body.data) ? body.data : [body.data];
const data = await Bluebird.mapSeries(dataArray, getAuthorCounts);
const lookup = dataArray.reduce((sum: any, authorId: string, i: number) => {
sum[authorId] = data[i];
return sum;
}, {});
validateOutputAndSendResponse(lookup, res, next);
},
);
return router;
}
================================================
FILE: packages/backend-api/src/api/services/commentActions.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
import {User} from '../../models';
import {
CommentActions,
enqueueAddTagTask,
enqueueCommentAction,
enqueueConfirmTagTask,
enqueueRejectTagTask,
enqueueRemoveTagTask,
enqueueResetTagTask,
enqueueScoreAction,
ScoreActions,
} from '../../processing';
import { REPLY_SUCCESS } from '../constants';
import { dataSchema, validateRequest } from '../util/validation';
export const detailAddTagSchema = Joi.object({
tagId: Joi.string().required(),
annotationStart: Joi.number().required(),
annotationEnd: Joi.number().greater(Joi.ref('annotationStart')).required(),
});
export const commentActionSchema = Joi.object({
commentId: Joi.string().required(),
});
const validateCommentActionRequest = validateRequest(dataSchema(commentActionSchema));
const validateDetailRequest = (schema: Joi.Schema) => validateRequest(dataSchema(schema));
/**
* Queues an accept, reject, defer or highlight action. Accepts array of comment ids, or a single comment id.
*/
export function queueMainAction(action: CommentActions): express.RequestHandler {
return async ({ body, user }, res) => {
const dataArray = Array.isArray(body.data) ? body.data : [body.data];
const isBatchAction = (dataArray.length > 1);
for (const data of dataArray) {
const { commentId } = data;
const parsedCommentId = parseInt(commentId, 10);
await enqueueCommentAction(
action,
(user as User).id,
parsedCommentId,
isBatchAction,
body.runImmediately);
}
res.json(REPLY_SUCCESS);
};
}
/**
* Queues an tag action. Accepts array of comment ids, or a single comment id.
*/
export function queueScoreCommentSummaryAction(action: ScoreActions): express.RequestHandler {
return async ({ body, params, user }, res) => {
const dataArray = Array.isArray(body.data) ? body.data : [body.data];
const parsedTagId = parseInt(params.tagid, 10);
for (const { commentId } of dataArray) {
const parsedCommentId = parseInt(commentId, 10);
await enqueueScoreAction(
action,
(user as User).id,
parsedCommentId,
parsedTagId,
body.runImmediately);
}
res.json(REPLY_SUCCESS);
};
}
/**
* Queues an tag action. Accepts array of comment ids, or a single comment id.
*/
export function queueSingleScoreAction(action: ScoreActions): express.RequestHandler {
return async ({ body, params, user }, res) => {
const parsedCommentId = parseInt(params.commentid, 10);
const parsedTagId = parseInt(params.tagid, 10);
await enqueueScoreAction(
action,
(user as User).id,
parsedCommentId,
parsedTagId,
body.runImmediately);
res.json(REPLY_SUCCESS);
};
}
export function createCommentActionsService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.post('/reset',
validateCommentActionRequest,
queueMainAction('resetComments'),
);
router.post('/approve',
validateCommentActionRequest,
queueMainAction('acceptComments'),
);
router.post('/approve-flags',
validateCommentActionRequest,
queueMainAction('acceptCommentsAndFlags'),
);
router.post('/resolve-flags',
validateCommentActionRequest,
queueMainAction('resolveFlags'),
);
router.post('/highlight',
validateCommentActionRequest,
queueMainAction('highlightComments'),
);
router.post('/reject',
validateCommentActionRequest,
queueMainAction('rejectComments'),
);
router.post('/reject-flags',
validateCommentActionRequest,
queueMainAction('rejectCommentsAndFlags'),
);
router.post('/defer',
validateCommentActionRequest,
queueMainAction('deferComments'),
);
router.post('/tag/:tagid',
validateCommentActionRequest,
queueScoreCommentSummaryAction('tagComments'),
);
router.post('/tagCommentSummaryScores/:tagid',
validateCommentActionRequest,
queueScoreCommentSummaryAction('tagCommentSummaryScores'),
);
router.post('/:commentid/tagCommentSummaryScores/:tagid/confirm',
queueSingleScoreAction('confirmCommentSummaryScore'),
);
router.post('/:commentid/tagCommentSummaryScores/:tagid/reject',
queueSingleScoreAction('rejectCommentSummaryScore'),
);
router.post('/:commentid/scores',
validateDetailRequest(detailAddTagSchema),
async ({ body, params, user }, res) => {
await enqueueAddTagTask(
parseInt(params.commentid, 10),
parseInt(body.data.tagId, 10),
(user as User).id,
body.data.annotationStart,
body.data.annotationEnd,
body.runImmediately);
res.json(REPLY_SUCCESS);
},
);
router.post('/:commentid/scores/:commentscoreid/reset',
async ({ body, params}, res) => {
await enqueueResetTagTask(parseInt(params.commentscoreid, 10), body.runImmediately);
res.json(REPLY_SUCCESS);
},
);
router.post('/:commentid/scores/:commentscoreid/confirm',
async ({ body, params, user}, res) => {
await enqueueConfirmTagTask(
(user as User).id,
parseInt(params.commentscoreid, 10),
body.runImmediately);
res.json(REPLY_SUCCESS);
},
);
router.post('/:commentid/scores/:commentscoreid/reject',
async ({ body, params, user}, res) => {
await enqueueRejectTagTask(
(user as User).id,
parseInt(params.commentscoreid, 10),
body.runImmediately);
res.json(REPLY_SUCCESS);
},
);
router.delete('/:commentid/scores/:commentscoreid',
async ({ body, params}, res) => {
await enqueueRemoveTagTask(parseInt(params.commentscoreid, 10), body.runImmediately);
res.json(REPLY_SUCCESS);
},
);
return router;
}
================================================
FILE: packages/backend-api/src/api/services/commentSources.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import { youtubeActivateChannel, youtubeSynchronizeChannel } from '../../integrations';
import {
Category,
User,
USER_GROUP_YOUTUBE,
} from '../../models';
import { enqueue, registerTask } from '../../processing/util';
import { REPLY_SUCCESS } from '../constants';
/**
* API endpoints to control comment sources.
*/
const ACTION_ACTIVATE = 'activate';
const ACTION_SYNC = 'sync';
export interface ISynchronizeChannelData {
ownerId: number;
channelId: number;
}
async function _youtubeSynchronizeChannel(
owner: User,
channel: Category,
) {
await enqueue('youtubeSynchronizeChannel', {ownerId: owner.id, channelId: channel.id});
}
registerTask('youtubeSynchronizeChannel', async (data: ISynchronizeChannelData) => {
const owner = await User.findByPk(data.ownerId);
const channel = await Category.findByPk(data.channelId);
if (!owner) {
throw new Error(`Youtube Sync failed: Owner ${data.ownerId} does not exist`);
}
if (!channel) {
throw new Error(`Youtube Sync failed: Channel ${data.channelId} does not exist`);
}
await youtubeSynchronizeChannel(owner, channel);
});
const ACTIONS = new Map([
[ACTION_ACTIVATE, new Map([[USER_GROUP_YOUTUBE, youtubeActivateChannel]])],
[ACTION_SYNC, new Map([[USER_GROUP_YOUTUBE, _youtubeSynchronizeChannel]])],
]);
function createAction(actionId: string): express.RequestHandler {
return async (req, res, next) => {
const category = await Category.findOne({where: {id: req.params.categoryId}});
if (!category) {
res.status(400).json({error: 'No such category'});
next();
return;
}
const owner = await category.getOwner();
if (!owner) {
res.status(400).json({error: 'Category has no owner'});
next();
return;
}
const action = ACTIONS.get(actionId)!.get(owner.group);
if (!action) {
res.status(400).json({error: `Category does not support action ${action}`});
next();
return;
}
try {
await action(owner, category, req.body.data);
}
catch (e) {
res.status(400).json({error: `Something went wrong: ${e}`});
next();
return;
}
res.json(REPLY_SUCCESS);
next();
};
}
export function createCommentSourcesService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.post('/activate/:categoryId', createAction(ACTION_ACTIVATE));
router.get('/sync/:categoryId', createAction(ACTION_SYNC));
return router;
}
================================================
FILE: packages/backend-api/src/api/services/editComment.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
import { logger } from '../../logger';
import { Comment } from '../../models';
import { enqueueSendCommentForScoringTask } from '../../processing';
import { REPLY_SUCCESS } from '../constants';
import { validateRequest } from '../util/validation';
const validateEditCommentRequest = validateRequest(Joi.object({
data: Joi.object({
commentId: Joi.string().required(),
text: Joi.string(),
authorName: Joi.string(),
authorLocation: Joi.string(),
}),
}));
/**
* Service route for editing comment text.
*/
export function createEditCommentTextService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.patch(
'/',
validateEditCommentRequest,
async ({ body }, res) => {
try {
const { commentId, text, authorName, authorLocation } = body.data;
const parsedCommentId = parseInt(commentId, 10);
const comment = await Comment.findByPk(parsedCommentId);
if (!comment) {
res.status(404).json({ status: 'error', errors: 'comment not found' });
return;
}
const author = {
...comment.author,
name: authorName ? authorName : comment.author.name,
location: authorLocation ? authorLocation : comment.author.location,
};
// update text and author fields of a comment
await comment.update({
text,
author,
});
enqueueSendCommentForScoringTask(commentId);
} catch (err) {
logger.error('Edit Comment error: ', err.name, err.message);
return;
}
res.status(200).json(REPLY_SUCCESS);
},
);
return router;
}
================================================
FILE: packages/backend-api/src/api/services/histogramScores/index.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
import {SUMMARY_SCORE_TAG} from '../../../models';
import { validateAndSendResponse } from '../../util/validation';
import {
getHistogramScoresForArticle,
getHistogramScoresForArticleByDate,
getHistogramScoresForCategory,
getHistogramScoresForCategoryByDate,
getMaxSummaryScoreForArticle,
getMaxSummaryScoreForCategory,
ICommentDated,
ICommentScored, NotFoundError,
renderScoresToPNG,
sortComments,
} from './util';
export interface ICommentScoredOrDatedWithStringId {
commentId: string;
}
export interface ICommentScoredWithStringId extends ICommentScoredOrDatedWithStringId {
score: number;
}
export interface ICommentDatedWithStringId extends ICommentScoredOrDatedWithStringId {
date: string;
}
const validateScoredCommentsAndSendResponse = validateAndSendResponse>(
Joi.array().items(
Joi.object().keys({
commentId: Joi.string().required(),
score: Joi.number().required(),
}),
),
);
const validateDatedCommentsAndSendResponse = validateAndSendResponse>(
Joi.array().items(
Joi.object().keys({
commentId: Joi.string().required(),
date: Joi.date().required(),
}),
),
);
function stringifyIds(arr: Array): Array;
function stringifyIds(arr: Array): Array;
function stringifyIds(arr: Array): Array {
return arr.map((a) => {
return Object.assign({}, a, {
commentId: a.commentId.toString(),
});
});
}
async function scoresToChart(
groupBy: 'date' | 'score',
getter: () => Promise>,
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
try {
const data = await getter();
const { query: { width, height, columnCount, showAll } } = req;
const parsedWidth = width ? parseInt(width as string, 10) : undefined;
const parsedHeight = height ? parseInt(height as string, 10) : undefined;
const parsedColumnCount = columnCount ? parseInt(columnCount as string, 10) : undefined;
const parsedShowAll = showAll ? (showAll === 'true') : false;
res.setHeader('Content-Type', 'image/png');
renderScoresToPNG(
data,
groupBy,
parsedWidth,
parsedHeight,
parsedColumnCount,
parsedShowAll,
).pngStream().pipe(res);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
}
export function createHistogramScoresService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get('/categories/:id/byDate', async ({ params: { id }, query: { sort }}, res, next) => {
try {
const categoryId = id === 'all' ? id : parseInt(id, 10);
const data = await getHistogramScoresForCategoryByDate(categoryId);
const sortedData = await sortComments(data, sort as string);
validateDatedCommentsAndSendResponse(stringifyIds(sortedData), res, next);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
});
router.get('/categories/:id/byDate/chart', async (req, res, next) => {
return scoresToChart('date', () => {
const { params: { id }} = req;
const categoryId = id === 'all' ? id : parseInt(id, 10);
return getHistogramScoresForCategoryByDate(categoryId);
}, req, res, next);
});
router.get('/categories/:id/tags/:tagId', async ({ params: { id, tagId }, query: { sort }}, res, next) => {
try {
const categoryId = id === 'all' ? id : parseInt(id, 10);
const tagIdNumber = parseInt(tagId, 10);
const data = await getHistogramScoresForCategory(categoryId, tagIdNumber);
const sortedData = await sortComments(data, sort as string);
validateScoredCommentsAndSendResponse(stringifyIds(sortedData), res, next);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
});
router.get('/categories/:id/summaryScore', async ({ params: { id }, query: { sort }}, res, next) => {
try {
const categoryId = id === 'all' ? id : parseInt(id, 10);
const data = await getMaxSummaryScoreForCategory(categoryId);
const sortedData = await sortComments(data, sort as string);
validateScoredCommentsAndSendResponse(stringifyIds(sortedData), res, next);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
});
router.get('/categories/:id/tags/:tagId/chart', async (req, res, next) => {
return scoresToChart('score', () => {
const { params: { id, tagId }} = req;
const categoryId = id === 'all' ? id : parseInt(id, 10);
if (tagId === SUMMARY_SCORE_TAG) {
return getMaxSummaryScoreForCategory(categoryId);
}
const tagIdNumber = parseInt(tagId, 10);
return getHistogramScoresForCategory(categoryId, tagIdNumber);
}, req, res, next);
});
router.get('/articles/:id/byDate', async ({ params: { id }, query: { sort }}, res, next) => {
try {
const data = await getHistogramScoresForArticleByDate(parseInt(id, 10));
const sortedData = await sortComments(data, sort as string);
validateDatedCommentsAndSendResponse(stringifyIds(sortedData), res, next);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
});
router.get('/articles/:id/byDate/chart', async (req, res, next) => {
return scoresToChart('date', () => {
const { params: { id } } = req;
return getHistogramScoresForArticleByDate(parseInt(id, 10));
}, req, res, next);
});
router.get('/articles/:id/tags/:tagId', async ({ params, query}, res, next) => {
const { id, tagId } = params;
const sort = query.sort as string;
const tagIdNumber = parseInt(tagId, 10);
try {
const data = await getHistogramScoresForArticle(parseInt(id, 10), tagIdNumber);
const sortedData = await sortComments(data, sort);
validateScoredCommentsAndSendResponse(stringifyIds(sortedData), res, next);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
});
router.get('/articles/:id/tags/:tagId/chart', async (req, res, next) => {
return scoresToChart('score', () => {
const { params: { id, tagId } } = req;
const articleId = parseInt(id, 10);
if (tagId === SUMMARY_SCORE_TAG) {
return getMaxSummaryScoreForArticle(articleId);
}
if (tagId === 'DATE') {
return getHistogramScoresForArticleByDate(articleId);
}
const tagIdNumber = parseInt(tagId, 10);
return getHistogramScoresForArticle(articleId, tagIdNumber);
}, req, res, next);
});
router.get('/articles/:id/summaryScore', async ({ params, query}, res, next) => {
try {
const { id } = params;
const sort = query.sort as string;
const data = await getMaxSummaryScoreForArticle(parseInt(id, 10));
const sortedData = await sortComments(data, sort);
validateScoredCommentsAndSendResponse(stringifyIds(sortedData), res, next);
} catch (e) {
if (e instanceof NotFoundError) {
res.status(404).send(e.message);
next();
} else {
next(e);
}
}
});
return router;
}
================================================
FILE: packages/backend-api/src/api/services/histogramScores/util.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const { Canvas } = require('canvas');
import { QueryTypes } from 'sequelize';
import { DotChartRenderer, groupByDateColumns, groupByScoreColumns } from '@conversationai/moderator-frontend-web';
import { Article, Category, Tag } from '../../../models';
import { sequelize } from '../../../sequelize';
import { sortCommentIds } from '../../util/sortCommentIds';
export interface ICommentScoredOrDated {
commentId: number;
}
export interface ICommentScored extends ICommentScoredOrDated {
score: number;
}
export interface ICommentDated extends ICommentScoredOrDated {
date: string;
}
export class NotFoundError extends Error {
}
export async function sortComments(data: Array, sortQuery?: string): Promise>;
export async function sortComments(data: Array, sortQuery?: string): Promise>;
export async function sortComments(data: Array, sortQuery?: string): Promise> {
if ((sortQuery === 'score') || (sortQuery === '-score')) {
// Any here, because the above check already proves its a ICommentScored
let sortedByScore = data.sort((a: any, b: any) => a.score - b.score);
if (sortQuery === '-score') {
sortedByScore = sortedByScore.reverse();
}
return sortedByScore;
}
if (!sortQuery) {
return data;
}
const sortedIds = await sortCommentIds(
data.map((d) => d.commentId),
sortQuery.split(','),
);
return data.sort((a, b) => {
return sortedIds.indexOf(a.commentId) - sortedIds.indexOf(b.commentId);
});
}
/**
* Get the max score for each comment across all categories given a tag.
*/
export async function getHistogramScoresForAllCategories(tagId: number): Promise> {
const tag = await Tag.findByPk(tagId);
if (!tag) { throw new NotFoundError(`Could not find tag ${tagId}`); }
return sequelize.query(
'SELECT comment_summary_scores.score AS score, comment_summary_scores.commentId ' +
'FROM comments ' +
'JOIN comment_summary_scores ON comment_summary_scores.commentId = comments.id ' +
`AND comment_summary_scores.tagId = :tagId ` +
'WHERE comments.isScored = true ' +
'AND comments.isModerated = false ' +
'ORDER BY score DESC',
{
replacements: {
tagId,
},
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment across all categories.
*/
export async function getHistogramScoresForAllCategoriesByDate(): Promise> {
return sequelize.query(
'SELECT comments.id as commentId, comments.sourceCreatedAt as date ' +
'FROM comments ' +
'WHERE comments.isModerated = false ',
{
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment in a category given a tag. If `categoryId` is the
* string value "all", then this just calls `getHistogramScoresForAllCategories`.
*/
export async function getHistogramScoresForCategory(categoryId: number | 'all', tagId: number): Promise> {
if (categoryId === 'all') {
return getHistogramScoresForAllCategories(tagId);
}
const category = await Category.findByPk(categoryId);
if (!category) { throw new NotFoundError(`Could not find category ${categoryId}`); }
const tag = await Tag.findByPk(tagId);
if (!tag) { throw new NotFoundError(`Could not find tag ${tagId}`); }
return sequelize.query(
'SELECT comment_summary_scores.score AS score, comment_summary_scores.commentId ' +
'FROM comments ' +
'JOIN articles ON articles.id = comments.articleId ' +
'JOIN comment_summary_scores ON comment_summary_scores.commentId = comments.id ' +
`AND comment_summary_scores.tagId = :tagId ` +
'WHERE articles.categoryId = :categoryId ' +
'AND comments.isScored = true ' +
'AND comments.isModerated = false ' +
'ORDER BY score DESC',
{
replacements: {
categoryId,
tagId,
},
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment in a category. If `categoryId` is the
* string value "all", then this just calls `getHistogramScoresForAllCategoriesByDate`.
*/
export async function getHistogramScoresForCategoryByDate(categoryId: number | 'all'): Promise> {
if (categoryId === 'all') {
return getHistogramScoresForAllCategoriesByDate();
}
const category = await Category.findByPk(categoryId);
if (!category) { throw new NotFoundError(`Could not find category ${categoryId}`); }
return sequelize.query(
'SELECT comments.id as commentId, comments.sourceCreatedAt as date ' +
'FROM comments ' +
'JOIN articles ON articles.id = comments.articleId ' +
'WHERE articles.categoryId = :categoryId ' +
'AND comments.isModerated = false ',
{
replacements: {
categoryId,
},
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment in an article given a tag.
*/
export async function getHistogramScoresForArticle(articleId: number, tagId: number): Promise> {
const article = await Article.findByPk(articleId);
if (!article) { throw new NotFoundError(`Could not find article ${articleId}`); }
const tag = await Tag.findByPk(tagId);
if (!tag) { throw new NotFoundError(`Could not find tag ${tagId}`); }
return sequelize.query(
'SELECT comment_summary_scores.score AS score, comment_summary_scores.commentId ' +
'FROM comments ' +
'JOIN comment_summary_scores ON comment_summary_scores.commentId = comments.id ' +
`AND comment_summary_scores.tagId = :tagId ` +
'WHERE comments.articleId = :articleId ' +
'AND comments.isScored = true ' +
'AND comments.isModerated = false ' +
'ORDER BY score DESC',
{
replacements: {
articleId,
tagId,
},
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment in an article, regardless of state or tag.
*/
export async function getHistogramScoresForArticleByDate(articleId: number): Promise> {
const article = await Article.findByPk(articleId);
if (!article) { throw new NotFoundError(`Could not find article ${articleId}`); }
return sequelize.query(
'SELECT comments.id as commentId, comments.sourceCreatedAt as date ' +
'FROM comments ' +
'WHERE comments.articleId = :articleId ' +
'AND comments.isModerated = false ',
{
replacements: {
articleId,
},
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max summary score for each comment in an article, regardless of state or tag.
*/
export async function getMaxSummaryScoreForArticle(articleId: number): Promise> {
const article = await Article.findByPk(articleId);
if (!article) { throw new NotFoundError(`Could not find article ${articleId}`); }
return sequelize.query(
'SELECT comments.id as commentId, comments.maxSummaryScore as score ' +
'FROM comments ' +
'WHERE comments.articleId = :articleId ' +
'AND comments.isScored = true ' +
'AND comments.isModerated = false ' +
'AND comments.maxSummaryScore IS NOT NULL',
{
replacements: {
articleId,
},
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment across all categories given a tag.
*/
export async function getMaxHistogramScoresForAllCategories(): Promise> {
return sequelize.query(
'SELECT comments.id as commentId, comments.maxSummaryScore as score ' +
'FROM comments ' +
'WHERE comments.isScored = true ' +
'AND comments.isModerated = false ' +
'AND comments.maxSummaryScore IS NOT NULL ' +
'ORDER BY score DESC',
{
type: QueryTypes.SELECT,
},
);
}
/**
* Get the max score for each comment in a category given a tag. If `categoryId` is the
* string value "all", then this just calls `getHistogramScoresForAllCategories`.
*/
export async function getMaxSummaryScoreForCategory(categoryId: number | 'all'): Promise> {
if (categoryId === 'all') {
return getMaxHistogramScoresForAllCategories();
}
const category = await Category.findByPk(categoryId);
if (!category) { throw new NotFoundError(`Could not find category ${categoryId}`); }
return sequelize.query(
'SELECT comments.id as commentId, comments.maxSummaryScore as score ' +
'FROM comments ' +
'JOIN articles ON articles.id = comments.articleId ' +
'WHERE articles.categoryId = :categoryId ' +
'AND comments.isScored = true ' +
'AND comments.isModerated = false ' +
'AND comments.maxSummaryScore IS NOT NULL ' +
'ORDER BY score DESC',
{
replacements: {
categoryId,
},
type: QueryTypes.SELECT,
},
);
}
const DEFAULT_IMAGE_WIDTH = 400;
const DEFAULT_IMAGE_HEIGHT = 200;
const DEFAULT_COLUMN_COUNT = 100;
export function renderScoresToPNG(
scores: Array,
groupBy: 'date' | 'score',
width?: number,
height?: number,
columnCount?: number,
showAll?: boolean,
) {
const w = width || DEFAULT_IMAGE_WIDTH;
const h = height || DEFAULT_IMAGE_HEIGHT;
const colCount = columnCount || DEFAULT_COLUMN_COUNT;
const commentsByColumn = groupBy === 'date'
? groupByDateColumns(scores, colCount)
: groupByScoreColumns(scores, colCount);
const canvas = new Canvas(w, h);
const renderer = new DotChartRenderer(
(canvasWidth, canvasHeight) => new Canvas(canvasWidth, canvasHeight),
);
renderer.setProps({
canvas,
commentsByColumn,
width: w,
height: h,
columnCount: colCount,
selectedRangeStart: 0,
selectedRangeEnd: 1,
showAll,
});
return canvas;
}
================================================
FILE: packages/backend-api/src/api/services/index.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import { processingTriggers } from '../../processing/api';
import { createAssignmentsService } from './assignments';
import { createAuthorCountsService } from './authorCounts';
import { createCommentActionsService } from './commentActions';
import { createCommentSourcesService } from './commentSources';
import { createEditCommentTextService } from './editComment';
import { createHistogramScoresService } from './histogramScores';
import { createModeratedCountsService } from './moderatedCounts';
import { createSearchService } from './search';
import { createSimpleRESTService } from './simple';
import { createTextSizesService } from './textSizes';
import { createUpdateNotificationService } from './updateNotifications';
export function createServicesRouter(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.use('/assignments', createAssignmentsService());
router.use('/search', createSearchService());
router.use('/commentActions', createCommentActionsService());
router.use('/histogramScores', createHistogramScoresService());
router.use('/moderatedCounts', createModeratedCountsService());
router.use('/authorCounts', createAuthorCountsService());
router.use('/textSizes', createTextSizesService());
router.use('/editComment', createEditCommentTextService());
router.use('/updates', createUpdateNotificationService());
router.use('/simple', createSimpleRESTService());
router.use('/processing', processingTriggers());
router.use('/comment_sources', createCommentSourcesService());
return router;
}
================================================
FILE: packages/backend-api/src/api/services/moderatedCounts.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
import { mapValues } from 'lodash';
import { Article, Category, Comment } from '../../models';
import { sortCommentIds } from '../util/sortCommentIds';
import { validateAndSendResponse } from '../util/validation';
interface IModeratedCounts {
approved: Array;
highlighted: Array;
rejected: Array;
deferred: Array;
flagged: Array;
batched: Array;
automated: Array;
}
interface IModeratedCountsAsStrings {
approved: Array;
highlighted: Array;
rejected: Array;
deferred: Array;
flagged: Array;
batched: Array;
automated: Array;
}
const validateCountsAndSendResponse = validateAndSendResponse(
Joi.object({
approved: Joi.array().items(Joi.string()).required(),
highlighted: Joi.array().items(Joi.string()).required(),
rejected: Joi.array().items(Joi.string()).required(),
deferred: Joi.array().items(Joi.string()).required(),
flagged: Joi.array().items(Joi.string()).required(),
batched: Joi.array().items(Joi.string()).required(),
automated: Joi.array().items(Joi.string()).required(),
}).required(),
);
async function getModeratedCounts(model: any, sortQuery: string, getWhere: (model: any, params: any) => Promise): Promise {
const results = await Promise.all([
// approved
getWhere(model, { isModerated: true, isAccepted: true }),
// highlighted
getWhere(model, { isModerated: true, isHighlighted: true }),
// rejected
getWhere(model, { isModerated: true, isAccepted: false }),
// deferred
getWhere(model, { isModerated: true, isDeferred: true }),
// flagged
getWhere(model, { unresolvedFlagsCount: {$gt: 0} }),
// batched
getWhere(model, { isModerated: true, isBatchResolved: true }),
// automated
getWhere(model, { isModerated: true, isAutoResolved: true }),
]);
const output = [];
for (const r of results) {
const ids = r.map((c: any) => c.id);
if (sortQuery) {
const sortedIds = await sortCommentIds(
ids,
sortQuery.split(','),
);
output.push(sortedIds);
} else {
output.push(ids);
}
}
return {
approved: output[0],
highlighted: output[1],
rejected: output[2],
deferred: output[3],
flagged: output[4],
batched: output[5],
automated: output[6],
};
}
export function createModeratedCountsService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get('/articles/:id', async ({ params: { id }, query: { sort }}, res, next) => {
let model;
try {
model = await Article.findByPk(id);
} catch (e) {
return Promise.reject({ error: 404 });
}
const data = await getModeratedCounts(model, sort as string, async (article, where) => {
return await article.getComments({
where,
attributes: ['id'],
});
});
const countsOfStrings: IModeratedCountsAsStrings = mapValues(data, (ids) => {
return ids.map((i: number) => i.toString());
}) as any;
validateCountsAndSendResponse(countsOfStrings, res, next);
});
router.get('/categories/:id', async ({ params: { id }, query: { sort }}, res, next) => {
let data;
if (id !== 'all') {
let model;
try {
model = await Category.findByPk(id);
} catch (e) {
return Promise.reject({ error: 404 });
}
data = await getModeratedCounts(model, sort as string, async (_article, where) => {
return await Comment.findAll({
where,
include: {
model: Article,
where: { categoryId: id },
attributes: ['id'],
} as any,
attributes: ['id'],
});
});
} else {
data = await getModeratedCounts(null, sort as string, async (_, where) => {
return Comment.findAll({
where,
attributes: ['id'],
});
});
}
const countsOfStrings: IModeratedCountsAsStrings = mapValues(data, (ids) => {
return ids.map((i: number) => i.toString());
}) as any;
validateCountsAndSendResponse(countsOfStrings, res, next);
});
return router;
}
================================================
FILE: packages/backend-api/src/api/services/search.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
import { QueryTypes } from 'sequelize';
import { logger } from '../../logger';
import { sequelize } from '../../sequelize';
import { sortCommentIds } from '../util/sortCommentIds';
import { validateAndSendResponse } from '../util/validation';
const MINIMUM_QUERY_LENGTH = 3;
type ISearchResponse = Array;
const validateSearchAndSendResponse = validateAndSendResponse(
Joi.array().items(Joi.string()),
);
export function createSearchService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get('/', async (req, res, next) => {
try {
const term = req.query.term as string;
const articleId = req.query.articleId as string;
const searchByAuthor = req.query.searchByAuthor as string;
let ids: Array = [];
let results;
if (term.length >= MINIMUM_QUERY_LENGTH) {
if (searchByAuthor === 'true') {
const iffyTerm = `%${term}%`;
results = await sequelize.query(
`SELECT id ` +
`FROM comments ` +
'WHERE comments.authorSourceId=:term ' +
'OR JSON_SEARCH(LOWER(comments.author), "all", LOWER(:iffyTerm), NULL, "$.name") IS NOT NULL ' +
'LIMIT 100',
{
replacements: {
iffyTerm,
term,
},
type: QueryTypes.SELECT,
},
);
} else if (articleId) {
results = await sequelize.query(
`SELECT id, MATCH(text) AGAINST (:term) as relevance ` +
`FROM comments ` +
'WHERE comments.articleId = :articleId ' +
`AND MATCH(text) AGAINST (:term) ` +
'ORDER BY relevance DESC ' +
'LIMIT 100',
{
replacements: {
term,
articleId,
},
type: QueryTypes.SELECT,
},
);
} else {
results = await sequelize.query(
`SELECT id, MATCH(text) AGAINST (:term) as relevance ` +
`FROM comments WHERE MATCH(text) AGAINST (:term) ` +
'ORDER BY relevance DESC ' +
'LIMIT 100',
{
replacements: {
term,
},
type: QueryTypes.SELECT,
},
);
}
}
ids = results ? results.map((r: any) => r.id) : [];
const sortQuery = req.query.sort as string;
const sortOrder = sortQuery ? sortQuery.split(',') : null;
// No sort order is specified, defaults to relevance
if (sortOrder == null) {
validateSearchAndSendResponse(
ids.map((i) => i.toString()),
res,
next,
);
} else {
const sortedIds = await sortCommentIds(
ids,
sortOrder,
);
validateSearchAndSendResponse(
sortedIds.map((i) => i.toString()),
res,
next,
);
}
} catch (e) {
logger.error(`Error with request posted to /services/search: ${e.message}`);
res.status(400).json({ status: 'error', errors: 'Search request not completed'});
return;
}
});
return router;
}
================================================
FILE: packages/backend-api/src/api/services/serializer.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { pick } from 'lodash';
import { Model } from 'sequelize';
export const TAG_FIELDS = ['id', 'color', 'description', 'key', 'label', 'isInBatchView', 'inSummaryScore', 'isTaggable'];
export const RANGE_FIELDS = ['id', 'categoryId', 'lowerThreshold', 'upperThreshold', 'tagId'];
export const TAGGING_SENSITIVITY_FIELDS = RANGE_FIELDS;
export const RULE_FIELDS = ['action', 'createdBy', ...RANGE_FIELDS];
export const PRESELECT_FIELDS = RANGE_FIELDS;
export const USER_FIELDS = ['id', 'name', 'email', 'avatarURL', 'group', 'isActive'];
const COMMENTSET_FIELDS = ['id', 'updatedAt', 'allCount', 'unprocessedCount', 'unmoderatedCount', 'moderatedCount',
'approvedCount', 'highlightedCount', 'rejectedCount', 'deferredCount', 'flaggedCount',
'batchedCount', 'recommendedCount', 'assignedModerators', ];
export const CATEGORY_FIELDS = [...COMMENTSET_FIELDS, 'label', 'ownerId', 'isActive', 'sourceId'];
export const ARTICLE_FIELDS = [...COMMENTSET_FIELDS, 'title', 'url', 'categoryId', 'sourceCreatedAt', 'lastModeratedAt',
'isCommentingEnabled', 'isAutoModerated'];
export const COMMENT_FIELDS = ['id', 'sourceId', 'replyToSourceId', 'replyId', 'authorSourceId', 'text', 'author',
'isScored', 'isModerated', 'isAccepted', 'isDeferred', 'isHighlighted', 'isBatchResolved', 'isAutoResolved',
'sourceCreatedAt', 'updatedAt', 'unresolvedFlagsCount', 'flagsSummary', 'sentForScoring', 'articleId',
'maxSummaryScore', 'maxSummaryScoreTagId',
];
export const SUMMARY_SCORE_FIELDS = ['tagId', 'score'];
export const SCORE_FIELDS = ['id', 'commentId', 'confirmedUserId', 'tagId', 'score',
'annotationStart', 'annotationEnd', 'sourceType', 'isConfirmed'];
export const FLAG_FIELDS = ['id', 'label', 'detail', 'isRecommendation', 'commentId', 'sourceId', 'authorSourceId',
'isResolved', 'resolvedById', 'resolvedAt'];
const ID_FIELDS = new Set(['id', 'categoryId', 'articleId', 'tagId', 'ownerId', 'commentId',
'confirmedUserId', 'resolvedById', 'replyId', 'maxSummaryScoreTagId']);
export type serializedData = {[key: string]: {} | Array | string | number};
// Convert IDs to strings, and assignedModerators to arrays of strings.
export function serialiseObject(
o: Model,
fields: Array,
): serializedData {
const serialised = pick(o.toJSON(), fields) as {[key: string]: any};
for (const k of Object.keys(serialised)) {
const v = serialised[k];
if (ID_FIELDS.has(k) && v) {
serialised[k] = v.toString();
}
}
if (serialised.assignedModerators) {
serialised.assignedModerators = serialised.assignedModerators.map(
(i: any) => (i.user_category_assignment ? i.user_category_assignment.userId.toString() :
i.moderator_assignment.userId.toString()));
}
return serialised;
}
================================================
FILE: packages/backend-api/src/api/services/simple.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* We use this module for API endpoints where it is easier to implement a custom interface
* than to modify/customise/configure the generic REST api to do the same thing.
*/
import * as express from 'express';
import { pick } from 'lodash';
import { Op, QueryTypes } from 'sequelize';
import {
checkModelType,
createRangeObject,
createTagObject,
deleteRangeObject,
modifyRangeObject,
modifyTagObject,
} from '../../actions/object_updaters';
import { createToken } from '../../auth/tokens';
import { clearError } from '../../integrations';
import {
Article,
Comment,
CommentFlag,
CommentScore,
CommentSummaryScore,
User,
USER_GROUP_ADMIN,
USER_GROUP_GENERAL,
USER_GROUP_SERVICE,
USER_GROUP_YOUTUBE,
} from '../../models';
import { sequelize } from '../../sequelize';
import { REPLY_SUCCESS } from '../constants';
import {
ARTICLE_FIELDS,
COMMENT_FIELDS,
FLAG_FIELDS,
SCORE_FIELDS,
serialiseObject,
serializedData,
SUMMARY_SCORE_FIELDS,
} from './serializer';
const userFields = ['id', 'name', 'email', 'group', 'isActive', 'extra'];
export function createSimpleRESTService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get('/systemUsers/:type', async (req, res) => {
const users = await User.findAll({
where: { group: req.params.type },
});
const userdata: Array = [];
for (const u of users) {
const simple = u.toJSON() as {[key: string]: any};
if (req.params.type === USER_GROUP_SERVICE) {
const token = await createToken(u.id);
simple.extra = {jwt: token};
}
else if (u.extra) {
simple.extra = u.extra;
// Make sure we don't send any access tokens out.
delete simple.extra.token;
}
userdata.push(pick(simple, userFields));
}
res.json({ users: userdata });
});
router.post('/user', async (req, res) => {
const {name, email, group, isActive} = req.body;
if (!(group === USER_GROUP_ADMIN || group === USER_GROUP_GENERAL || group === USER_GROUP_SERVICE)) {
res.status(400).send(`Can't create users of type ${group}`);
return;
}
if ((group === USER_GROUP_ADMIN || group === USER_GROUP_GENERAL) && !email) {
res.status(400).send('User creation error: Human users require an email.');
return;
}
if (email) {
const existing = await User.count({where: {email}});
if (existing) {
res.status(400).send('User creation error: email already in use.');
return;
}
}
await User.create({name, email, group, isActive});
res.json(REPLY_SUCCESS);
});
router.post('/user/:id', async (req, res) => {
const userId = parseInt(req.params.id, 10);
const user = await User.findByPk(userId);
if (!user) {
res.status(404).send('Not found');
return;
}
const group = user.group;
function isRealUser(g: string) {
return g === USER_GROUP_ADMIN || g === USER_GROUP_GENERAL;
}
if ((isRealUser(group) || group === USER_GROUP_SERVICE) && typeof(req.body.name) !== 'undefined') {
user.name = req.body.name;
}
if (isRealUser(group)) {
if (isRealUser(req.body.group)) {
user.group = req.body.group;
}
if (typeof(req.body.email) !== 'undefined') {
user.email = req.body.email;
}
}
if (typeof(req.body.isActive) !== 'undefined') {
user.isActive = req.body.isActive;
}
await user.save();
if (group === USER_GROUP_YOUTUBE && req.body.isActive) {
await clearError(user);
}
res.json(REPLY_SUCCESS);
});
router.post('/article/:id', async (req, res) => {
const articleId = parseInt(req.params.id, 10);
const article = await Article.findByPk(articleId);
if (!article) {
res.status(404).json({status: 'error', errors: 'article not found'});
return;
}
if (typeof req.body.isCommentingEnabled === 'boolean') {
article.isCommentingEnabled = req.body.isCommentingEnabled;
}
if (typeof req.body.isAutoModerated === 'boolean') {
article.isAutoModerated = req.body.isAutoModerated;
}
await article.save();
res.json(REPLY_SUCCESS);
});
router.post('/articles', async (req, res) => {
const articles = await Article.findAll({
where: {id: {[Op.in]: req.body}},
include: [{ model: User, as: 'assignedModerators', attributes: ['id']}],
});
const articleData = articles.map((a) => serialiseObject(a, ARTICLE_FIELDS));
res.json(articleData);
});
router.post('/comments', async (req, res) => {
const comments = await Comment.findAll({
where: {id: {[Op.in]: req.body}},
include: [
{model: Article, as: 'article', attributes: ['categoryId']},
{model: Comment, as: 'replies', attributes: ['id']},
],
});
const summaryScores = await CommentSummaryScore.findAll({
where: {commentId: {[Op.in]: req.body}},
});
const results = await sequelize.query(
'SELECT commentId, tagId, score, annotationStart, annotationEnd ' +
'FROM comment_scores ' +
'WHERE id IN (SELECT commentScoreId from comment_top_scores where commentId in (:commentIds))',
{
type: QueryTypes.SELECT,
replacements: { commentIds: req.body },
},
) as Array;
const topScores = new Map();
for (const topScore of results) {
topScores.set(
`${topScore.commentId}:${topScore.tagId}`,
{score: topScore.score, start: topScore.annotationStart, end: topScore.annotationEnd},
);
}
const scoresMap = new Map>();
for (const score of summaryScores) {
let scoresForComment = scoresMap.get(score.commentId);
if (!scoresForComment) {
scoresForComment = [];
scoresMap.set(score.commentId, scoresForComment);
}
const summaryScore = serialiseObject(score, SUMMARY_SCORE_FIELDS);
const topScore = topScores.get(`${score.commentId}:${score.tagId}`);
if (topScore) {
summaryScore['topScore'] = topScore;
}
scoresForComment.push(summaryScore);
}
const commentData = comments.map((c) => {
const data = serialiseObject(c, COMMENT_FIELDS);
if ((c as any).article && (c as any).article.categoryId) {
data['categoryId'] = (c as any).article.categoryId.toString();
}
if ((c as any).replies) {
data['replies'] = ((c as any).replies as Array).map((r) => r.id.toString());
}
const scoreData = scoresMap.get(c.id);
if (scoreData) {
data['summaryScores'] = scoreData;
}
return data;
});
res.json(commentData);
});
router.get('/article/:id/text', async (req, res) => {
const articleId = parseInt(req.params.id, 10);
const article = await Article.findByPk(articleId);
if (!article) {
res.status(404).send('Not found');
return;
}
const text = article.text;
res.json({text: text});
});
router.get('/comment/:id/scores', async (req, res) => {
const commentId = parseInt(req.params.id, 10);
const scores = await CommentScore.findAll({
where: {commentId: commentId},
});
const scoresData = scores.map((s) => serialiseObject(s, SCORE_FIELDS));
res.json(scoresData);
});
router.get('/comment/:id/flags', async (req, res) => {
const commentId = parseInt(req.params.id, 10);
const flags = await CommentFlag.findAll({
where: {commentId: commentId},
});
const flagsData = flags.map((f) => serialiseObject(f, FLAG_FIELDS));
res.json(flagsData);
});
router.post('/tag', async (req, res) => {
const msg = await createTagObject(req.body);
if (msg) {
res.status(400).send(msg);
return;
}
res.json(REPLY_SUCCESS);
});
router.patch('/tag/:id', async (req, res) => {
const id = parseInt(req.params.id, 10);
const msg = await modifyTagObject(id, req.body);
if (msg) {
res.status(400).send(msg);
return;
}
res.json(REPLY_SUCCESS);
});
router.post('/:model', async (req, res) => {
if (!checkModelType(req.params.model)) {
res.status(400).send('Bad object type');
return;
}
const msg = await createRangeObject(req.params.model, req.body);
if (msg) {
res.status(400).send(msg);
return;
}
res.json(REPLY_SUCCESS);
});
router.patch('/:model/:id', async (req, res) => {
const id = parseInt(req.params.id, 10);
if (!checkModelType(req.params.model)) {
res.status(400).send('Bad object type');
return;
}
const msg = await modifyRangeObject(req.params.model, id, req.body);
if (msg) {
res.status(400).send(msg);
return;
}
res.json(REPLY_SUCCESS);
});
router.delete('/:model/:id', async (req, res) => {
const objectId = parseInt(req.params.id, 10);
if (!checkModelType(req.params.model) && req.params.model !== 'tag') {
res.status(400).send('Bad object type');
return;
}
await deleteRangeObject(req.params.model, objectId);
res.json(REPLY_SUCCESS);
});
return router;
}
================================================
FILE: packages/backend-api/src/api/services/textSizes.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
import { Op } from 'sequelize';
import { CommentSize } from '../../models';
import { validateAndSendResponse, validateRequest } from '../util/validation';
const validateTextSizesRequest = validateRequest(Joi.object({
data: Joi.array().items(Joi.string()).required(),
}));
interface ITextSizesResponse {
[key: string]: number;
}
const validateTextSizesAndSendResponse = validateAndSendResponse(
Joi.object({
arg: Joi.string(),
value: Joi.number(),
}).unknown().required(),
);
export function createTextSizesService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.post('/',
validateTextSizesRequest,
async ({ body: { data }, query }, res, next) => {
const width = query.width as string;
try {
const widthNum = parseInt(width, 10);
if (isNaN(widthNum)) {
res.status(422).json({ status: 'error', errors: [`width query string param must be a number, got ${width}`] });
return;
}
const commentSizes = await CommentSize.findAll({
where: {
commentId: {
[Op.in]: data,
},
width: widthNum,
},
});
// Default all values to 60, the average height.
// This is in case a comment has not yet been cached.
const defaultData: ITextSizesResponse = data.reduce((sum: any, commentId: number) => {
sum[commentId] = 60;
return sum;
}, {});
const sizingData = commentSizes.reduce((sum, commentSize: CommentSize) => {
sum[commentSize.commentId.toString()] = commentSize.height;
return sum;
}, defaultData);
validateTextSizesAndSendResponse(sizingData, res, next);
} catch (e) {
next(e);
}
},
);
return router;
}
================================================
FILE: packages/backend-api/src/api/services/updateNotifications.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Set true to send test update packets
const SEND_TEST_UPDATE_PACKETS = false;
import * as express from 'express';
import { isEqual } from 'lodash';
import { Op } from 'sequelize';
import * as WebSocket from 'ws';
import { logger } from '../../logger';
import {
Article,
Category,
ModerationRule,
Preselect,
Tag,
TaggingSensitivity,
User,
} from '../../models';
import {clearInterested, INotificationData, registerInterest} from '../../notification_router';
import { countAssignments } from './assignments';
import {
ARTICLE_FIELDS,
CATEGORY_FIELDS,
PRESELECT_FIELDS,
RULE_FIELDS,
serialiseObject,
TAGGING_SENSITIVITY_FIELDS,
TAG_FIELDS,
USER_FIELDS,
} from './serializer';
// TODO: Can't find a good way to get rid of the any types below. And typing is generally a mess.
// Revisit when sequelize has been updated
interface ISystemData {
users: any;
tags: any;
taggingSensitivities: any;
rules: any;
preselects: any;
}
interface IAllArticlesData {
categories: any;
articles: any;
}
interface IArticleUpdateData {
category: any;
article: any;
}
interface IPerUserData {
assignments: number;
}
interface IMessage {
type: 'system' | 'global' | 'article-update' | 'user';
data: ISystemData | IAllArticlesData | IArticleUpdateData | IPerUserData;
}
async function getSystemData() {
const users = await User.findAll({where: {group: {[Op.in]: ['admin', 'general']}}});
const userdata = users.map((u: User) => {
return serialiseObject(u, USER_FIELDS);
});
const tags = await Tag.findAll({});
const tagdata = tags.map((t: Tag) => {
return serialiseObject(t, TAG_FIELDS);
});
const taggingSensitivities = await TaggingSensitivity.findAll({});
const tsdata = taggingSensitivities.map((t: TaggingSensitivity) => {
return serialiseObject(t, TAGGING_SENSITIVITY_FIELDS);
});
const rules = await ModerationRule.findAll({});
const ruledata = rules.map((r: ModerationRule) => {
return serialiseObject(r, RULE_FIELDS);
});
const preselects = await Preselect.findAll({});
const preselectdata = preselects.map((p: Preselect) => {
return serialiseObject(p, PRESELECT_FIELDS);
});
return {
type: 'system',
data: {
users: userdata,
tags: tagdata,
taggingSensitivities: tsdata,
rules: ruledata,
preselects: preselectdata,
},
} as IMessage;
}
async function getGlobalData() {
const categories = await Category.findAll({
include: [{ model: User, as: 'assignedModerators', attributes: ['id']}],
});
const categoryIds: Array = [];
const categorydata = categories.map((c: Category) => {
categoryIds.push(c.id);
return serialiseObject(c, CATEGORY_FIELDS);
});
const articles = await Article.findAll({
where: {[Op.or]: [{categoryId: null}, {categoryId: categoryIds}]},
include: [{ model: User, as: 'assignedModerators', attributes: ['id']}],
});
const articledata = articles.map((a: Article) => {
return serialiseObject(a, ARTICLE_FIELDS);
});
return {
type: 'global',
data: {
categories: categorydata,
articles: articledata,
},
} as IMessage;
}
async function getCategoryUpdate(categoryId: number) {
const category = await Category.findByPk(
categoryId,
{include: [{ model: User, as: 'assignedModerators', attributes: ['id']}]},
);
const cData = category ? serialiseObject(category, CATEGORY_FIELDS) : undefined;
return {
type: 'article-update',
data: {
categories: [cData],
articles: [],
},
} as IMessage;
}
async function getArticleUpdate(articleId: number) {
const article = await Article.findByPk(
articleId,
{include: [{ model: User, as: 'assignedModerators', attributes: ['id']}]},
);
if (!article) {
return null;
}
const aData = serialiseObject(article, ARTICLE_FIELDS);
const category = article.categoryId ? await Category.findByPk(
article.categoryId,
{include: [{ model: User, as: 'assignedModerators', attributes: ['id']}]},
) : null;
const cData = category ? serialiseObject(category, CATEGORY_FIELDS) : undefined;
return {
type: 'article-update',
data: {
categories: [cData],
articles: [aData],
},
} as IMessage;
}
async function getPerUserData(userId: number) {
const user = (await User.findByPk(userId))!;
const assignments = await countAssignments(user);
return {
type: 'user',
data: {
assignments: assignments,
},
} as IMessage;
}
interface ISocketItem {
userId: number;
ws: Array;
lastPerUserMessage: IPerUserData | null;
sentInitialMessages: boolean;
}
let lastSystemMessage: IMessage | null = null;
const socketItems = new Map();
async function refreshSystemMessage(): Promise {
const newMessage = await getSystemData();
if (!lastSystemMessage) {
lastSystemMessage = newMessage;
return true;
}
const send = !isEqual(newMessage.data, lastSystemMessage.data);
if (send) {
lastSystemMessage = newMessage;
}
return send;
}
function removeSocket(si: ISocketItem, ws: WebSocket) {
const index = si.ws.indexOf(ws);
if (index >= 0) {
si.ws.splice(index, 1);
}
if (si.ws.length === 0) {
socketItems.delete(si.userId);
}
}
async function refreshMessages(alwaysSend: boolean) {
const sendSystem = (await refreshSystemMessage() || alwaysSend);
return {sendSystem, sendUser: alwaysSend};
}
async function maybeSendUpdateToUser(si: ISocketItem,
{sendSystem, sendUser}:
{sendSystem: boolean, sendUser: boolean}) {
const userSummaryMessage = await getPerUserData(si.userId);
sendUser = sendUser || !si.lastPerUserMessage || !isEqual(userSummaryMessage.data, si.lastPerUserMessage);
for (const ws of si.ws) {
try {
if (sendSystem) {
logger.info(`Sending system data to user ${si.userId}`);
await ws.send(JSON.stringify(lastSystemMessage));
}
if (sendUser) {
logger.info(`Sending per user data to user ${si.userId}`);
await ws.send(JSON.stringify(userSummaryMessage));
}
}
catch (e) {
logger.warn(`Websocket faulty for ${si.userId}`, e.message);
ws.terminate();
removeSocket(si, ws);
}
}
si.lastPerUserMessage = userSummaryMessage.data as IPerUserData;
}
async function maybeSendUpdates() {
for (const si of socketItems.values()) {
const updateFlags = await refreshMessages(false);
await maybeSendUpdateToUser(si, updateFlags);
}
}
async function sendUpdate(type: string, update: string) {
for (const si of socketItems.values()) {
if (!si.sentInitialMessages) {
continue;
}
for (const ws of si.ws) {
logger.info(`Sending ${type} to user ${si.userId}`);
await ws.send(update);
}
}
}
async function sendGlobal() {
const update = await getGlobalData();
await sendUpdate('global', JSON.stringify(update));
}
async function sendCategoryUpdate(categoryId: number) {
const update = await getCategoryUpdate(categoryId);
await sendUpdate('category update', JSON.stringify(update));
}
async function sendArticleUpdate(articleId: number) {
const update = await getArticleUpdate(articleId);
await sendUpdate('article update', JSON.stringify(update));
}
function sendTestUpdatePackets(si: ISocketItem) {
logger.info(`*** settng up fake update notifications for user ${si.userId}`);
let counter = 1;
setInterval(async () => {
const update = await getArticleUpdate(counter);
if (!update) {
logger.info(`no such article ${counter}`);
counter = 1;
return;
}
const data = update.data as IArticleUpdateData;
let msg = `fake update message ${counter}`;
if (counter % 3 === 1) {
// Just send the category
delete data.article;
msg += ' category';
}
else if (counter % 3 === 2) {
// just send the article
delete data.category;
msg += ' article';
}
else {
msg += ' both';
}
if (counter % 4 === 2) {
// pretend its a new object
msg += ' new';
if (data.article) {
data.article.id = data.article.id + 10000 + Math.floor(Math.random() * 1000);
}
if (data.category) {
data.category.id = data.category.id + 10000 + Math.floor(Math.random() * 1000);
}
}
if (counter % 4 === 3) {
msg += ' faked data';
// Mess with the data
if (data.article) {
data.article.unmoderatedCount = data.article.unmoderatedCount + Math.floor(Math.random() * 10000);
}
if (data.category) {
data.category.unmoderatedCount = data.category.unmoderatedCount + Math.floor(Math.random() * 10000);
}
}
logger.info(msg);
counter ++;
logger.info(`Sending **fake** article update to user ${si.userId} -- ${msg}`);
for (const ws of si.ws) {
await ws.send(JSON.stringify(update));
}
}, 1000);
}
// Introduce a simple task queue to ensure messages are processed in the order in which they arrive
const taskQueue: Array<() => Promise> = [];
let taskQueueProcessing = false;
async function processNotification(data: INotificationData) {
if (data.objectType === 'category' && data.id) {
taskQueue.unshift(() => sendCategoryUpdate(data.id!));
} else if (data.objectType === 'article' && data.id) {
taskQueue.unshift(() => sendArticleUpdate(data.id!));
} else {
taskQueue.unshift(maybeSendUpdates);
}
if (taskQueueProcessing) {
return;
}
taskQueueProcessing = true;
while (taskQueue.length > 0) {
const task = taskQueue.pop();
await task!();
}
taskQueueProcessing = false;
}
let registered = false;
export function createUpdateNotificationService(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.ws('/summary', async (ws, req) => {
if (!req.user) {
logger.error(`Attempt to create a socket without authentication. Bail...`);
ws.terminate();
return;
}
const userId = (req.user as User).id;
let si = socketItems.get(userId);
if (!si) {
si = {userId, ws: [], lastPerUserMessage: null, sentInitialMessages: false};
socketItems.set(userId, si);
if (SEND_TEST_UPDATE_PACKETS) {
sendTestUpdatePackets(si);
}
}
si.ws.push(ws);
if (!registered) {
logger.info(`Setting up notifications`);
registerInterest({ processNotification });
registered = true;
}
ws.on('close', () => {
removeSocket(si!, ws);
});
logger.info(`Websocket opened to ${(req.user as User).email}`);
const updateFlags = await refreshMessages(true);
await maybeSendUpdateToUser(si, updateFlags);
si.sentInitialMessages = true;
await sendGlobal();
});
return router;
}
export function destroyUpdateNotificationService() {
registered = false;
clearInterested();
for (const si of socketItems.values()) {
for (const ws of si.ws) {
ws.close();
}
}
socketItems.clear();
}
================================================
FILE: packages/backend-api/src/api/util/permissions.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import {User} from '../../models';
export function onlyAdmin(req: express.Request, res: express.Response, next: express.NextFunction) {
if ((req as any).testMode) {
next();
return;
}
// TODO(ldixon): check that user is always defined; and if so update types.
if (['admin'].indexOf((req.user as User).group) === -1) {
res.status(403).json({ error: 'Only admin users can access this API.' });
} else {
next();
}
}
export function onlyServices(req: express.Request, res: express.Response, next: express.NextFunction) {
if ((req as any).testMode) {
next();
return;
}
// TODO(ldixon): check that user is always defined; and if so update types.
if (['service'].indexOf((req.user as User).group) === -1) {
res.status(403).json({ error: 'Only service users can access this API.' });
} else {
next();
}
}
export function onlyAdminAndServices(req: express.Request, res: express.Response, next: express.NextFunction) {
if ((req as any).testMode) {
next();
return;
}
// TODO(ldixon): check that user is always defined; and if so update types.
if (['service', 'admin'].indexOf((req.user as User).group) === -1) {
res.status(403).json({ error: 'General users cannot acces this API.' });
} else {
next();
}
}
================================================
FILE: packages/backend-api/src/api/util/server.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as bodyParser from 'body-parser';
import * as compression from 'compression';
import * as cors from 'cors';
import * as express from 'express';
import * as expressWinston from 'express-winston';
import * as expressWs from 'express-ws';
import * as helmet from 'helmet';
import { Server } from 'http';
import * as winston from 'winston';
// Logger to capture all requests and output them to the console.
export const requestLogger = expressWinston.logger({
transports: [
new winston.transports.Console({
format: winston.format.simple(),
}),
],
meta: false,
ignoredRoutes: [
'/_ah/health',
],
});
// Logger to capture any top-level errors and output json diagnostic info.
export const errorLogger = expressWinston.errorLogger({
transports: [
new winston.transports.Console({
format: winston.format.simple(),
}),
],
format: winston.format.combine(
winston.format.json(),
),
requestWhitelist: ['body'],
});
export function getExpressAppWithPreprocessors(testMode?: boolean) {
const app = express();
expressWs(app);
if (!testMode) {
// Turn on GZip.
app.use(compression());
}
// Required to parse JSON posts.
app.use(bodyParser.json({ limit: '2mb' }));
if (!testMode) {
// Enable CORS
app.use(cors());
app.options('*', cors());
app.use(helmet({
// TODO: Implement a proper content security policy
contentSecurityPolicy: false,
}));
app.use(requestLogger);
}
return app;
}
export function applyCommonPostprocessors(app: express.Application, testMode?: boolean) {
if (!testMode) {
// Add the error logger after all middleware and routes so that
// it can log errors from the whole application. Any custom error
// handlers should go after this.
app.use(errorLogger);
// Basic 404 handler
app.use((_req: express.Request, res: express.Response) => {
if (!res.headersSent) {
res.status(404).send('Not Found');
}
});
// Basic error handler
app.use((_err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
// If our routes specified a specific response, then send that. Otherwise,
// send a generic message so as not to leak anything.
if (!res.headersSent) {
res.status(500).json({error: 'Internal Server Error'});
}
});
}
}
export function makeServer(testMode?: boolean): {
app: express.Application;
start(port: number): Server;
} {
const app = getExpressAppWithPreprocessors(testMode);
return {
app,
start(port: number) {
applyCommonPostprocessors(app, testMode);
return app.listen(port, () => {
console.log('OSMod listening on port', port);
});
},
};
}
================================================
FILE: packages/backend-api/src/api/util/sortCommentIds.ts
================================================
/*
Copyright 2020 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {Op, OrderItem} from 'sequelize';
import {Comment} from '../../models';
export async function sortCommentIds(
ids: Array,
sort: Array,
): Promise> {
const order: Array = [];
for (let sortItem of sort) {
let orderItem = 'ASC';
if (sortItem.startsWith('-')) {
sortItem = sortItem.substring(1);
orderItem = 'DESC';
}
order.push([sortItem, orderItem]);
}
const items: Array<{id: number}> = await Comment.findAll({
where: { id: {[Op.in]: ids } },
order,
attributes: ['id'],
});
return items.map((item: any) => item.id);
}
================================================
FILE: packages/backend-api/src/api/util/validation.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as Joi from 'joi';
export function dataSchema(type: any) {
return Joi.object().keys({
runImmediately: Joi.boolean().optional(),
data: Joi.alternatives().try(
Joi.array().items(type),
type,
).required(),
});
}
/**
* Express middleware to make sure the body of the post is 1 or more author ids.
*/
export function validateRequest(schema: Joi.Schema) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const data = req.body;
const status = schema.validate(data, { convert: false });
if (status.error) {
// console.error(status.error.details);
res.status(422).json({ status: 'request validation error', errors: status.error.details, data });
return;
}
next();
};
}
export function validateAndSendResponse(schema: Joi.Schema) {
return (data: T, res: express.Response, next: express.NextFunction) => {
const status = schema.validate(data, { convert: false });
if (status.error) {
// console.error(status.error.details);
res.status(422).json({ status: 'response validation error', errors: status.error.details, data });
return;
}
res.json({ data });
next();
};
}
================================================
FILE: packages/backend-api/src/auth/config.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CONFIGURATION_GOOGLE_OAUTH, getConfigItem, setConfigItem } from '../models';
export interface IGoogleOAuthConfiguration {
id: string;
secret: string;
}
export async function getOAuthConfiguration() {
return await getConfigItem(CONFIGURATION_GOOGLE_OAUTH) as IGoogleOAuthConfiguration | null;
}
export async function setOAuthConfiguration(oauthConfig: IGoogleOAuthConfiguration) {
return await setConfigItem(CONFIGURATION_GOOGLE_OAUTH, oauthConfig);
}
let oauthGood = false;
export async function setOAuthGood(isGood: boolean) {
oauthGood = isGood;
}
export async function isOAuthGood() {
return oauthGood;
}
================================================
FILE: packages/backend-api/src/auth/providers/google.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { config } from '../../config';
import { User } from '../../models';
import { IGoogleOAuthConfiguration, setOAuthGood } from '../config';
import { ensureFirstUser, findOrCreateUserSocialAuth, isFirstUserInitialised } from '../users';
const Strategy = require('passport-google-oauth20').Strategy;
class AuthError extends Error {
}
export interface IGoogleProfile {
id: string;
displayName: string;
name: {
familyName: string;
givenName: string;
};
emails: Array<{
value: string;
verified: string;
}>;
photos: Array<{
value: string;
}>;
}
/**
* Map Google OAuth data to a data object to jam into a User model
*/
export function mapAuthDataToUser(profile: IGoogleProfile) {
const email = profile.emails[0].value;
return {
email,
name: profile.displayName,
};
}
/**
* Map Google OAuth data to a data object to jam into a UserSocialAuth model
*/
export function mapAuthDataToUserSocialAuth(accessToken: string, refreshToken: string, profile: IGoogleProfile) {
return {
provider: 'google',
socialId: profile.id,
extra: {
accessToken,
refreshToken,
profile,
},
};
}
/**
* Login verification after successful Google Oauth flow. Takes the passed in data an:
*
* 1. Finds or creates the user based on their email address
* 2. Finds or creates a social auth record and relates it to the user
*/
export async function verifyGoogleToken(accessToken: string, refreshToken: string, profile: IGoogleProfile): Promise {
const userData = mapAuthDataToUser(profile);
if (!userData) {
throw new Error('Error extracting user auth data');
}
if (!await isFirstUserInitialised()) {
await ensureFirstUser(userData);
}
await setOAuthGood(true);
const user = await User.findOne({
where: { email: userData.email },
});
if (!user) {
throw new AuthError(`User ${userData.email} is not yet registered with Moderator.`);
}
if (!user.isActive) {
throw new AuthError(`User ${userData.email} has been deactivated.`);
}
const userSocialAuthData = mapAuthDataToUserSocialAuth(accessToken, refreshToken, profile);
await findOrCreateUserSocialAuth(user, userSocialAuthData);
return user;
}
export function getGoogleStrategy(
oauthConfig: IGoogleOAuthConfiguration,
) {
return new Strategy(
{
clientID: oauthConfig.id,
clientSecret: oauthConfig.secret,
callbackURL: `${config.get('api_url')}/auth/callback/google`,
userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo',
},
async (accessToken: string, refreshToken: string, profile: IGoogleProfile,
callback: (err: any, user?: User | false, info?: any) => any) => {
try {
const user = await verifyGoogleToken(accessToken, refreshToken, profile);
// Sync avatar
if (profile.photos && profile.photos[0] && profile.photos[0].value) {
await user.update({avatarURL: profile.photos[0].value});
}
callback(null, user);
}
catch (e) {
if (e instanceof AuthError) {
callback(null, false, {reason: e.message});
}
else {
callback(e);
}
}
},
);
}
================================================
FILE: packages/backend-api/src/auth/providers/index.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * from './google';
export * from './jwt';
================================================
FILE: packages/backend-api/src/auth/providers/jwt.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ExtractJwt, Strategy } from 'passport-jwt';
import { User } from '../../models';
import { getTokenConfiguration, isValidToken } from '../tokens';
import { isValidUser } from '../users';
/**
* Verify JWT payload from JWT Passportstrategy
*
* @param {object} jwtPayload Decoded JWT payload
* @param {function} done Verification callback
*/
export async function verifyJWT(jwtPayload: any): Promise {
if (!isValidToken(jwtPayload)) {
throw new Error('Invalid token');
}
const user = await User.findByPk(jwtPayload.user);
if (user) {
if (isValidUser(user)) {
if (user.email) {
if (user.email === jwtPayload.email) {
return user;
} else {
throw new Error(`User email does not match token: ${user.email} === ${jwtPayload.email}`);
}
} else {
return user;
}
}
throw new Error('User not valid');
} else {
throw new Error('User not found');
}
}
/**
* JWT Passport strategy configuration
*/
export async function getJwtStrategy() {
const config = await getTokenConfiguration();
return new Strategy(
{
secretOrKey: config.secret,
issuer: config.issuer,
jwtFromRequest: (ExtractJwt as any).fromExtractors([
// Pull JWT token out of request header formatted like so: "Authorization: JWT (token)"
ExtractJwt.fromAuthHeaderWithScheme('jwt'),
// Or, grab from `token` query string.
ExtractJwt.fromUrlQueryParameter('token'),
]),
},
async (jwtPayload: any, callback: (err: any, user?: User | false) => any) => {
try {
const user = await verifyJWT(jwtPayload);
callback(null, user);
} catch (e) {
callback(e);
}
},
);
}
================================================
FILE: packages/backend-api/src/auth/router.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as passport from 'passport';
import * as qs from 'qs';
import { config } from '../config';
import { findOrCreateTagByKey, SUMMARY_SCORE_TAG, User } from '../models';
import { restartService } from '../server-management';
import {
getOAuthConfiguration,
IGoogleOAuthConfiguration,
isOAuthGood,
setOAuthConfiguration,
setOAuthGood,
} from './config';
import { createToken } from './tokens';
import { isFirstUserInitialised } from './users';
import { generateServerCSRF, getClientCSRF } from './utils';
function redirectToFrontend(
res: express.Response,
success: boolean,
params: object = {},
referrer?: string | null,
): void {
let redirectHost;
if (!referrer) {
redirectHost = config.get('frontend_url');
}
else {
redirectHost = referrer;
}
if (redirectHost === '') {
redirectHost = '/';
}
const queryString = qs.stringify(Object.assign({
error: !success,
}, params));
res.redirect(`${redirectHost}?${queryString}`);
}
export function createHealthcheckRouter(oauthConfig: IGoogleOAuthConfiguration | null): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get(
'/auth/healthcheck',
async (_req, res, next) => {
if (oauthConfig == null) {
res.status(218).send('init_oauth');
}
else if (!await isFirstUserInitialised()) {
res.status(218).send('init_first_user');
}
else if (!await isOAuthGood()) {
res.status(218).send('init_check_oauth');
}
else {
res.send('ok');
}
next();
},
);
return router;
}
export function createAuthConfigRouter(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get(
'/auth/config',
async (_req, res, next) => {
const data = await getOAuthConfiguration();
const id = (data && data.id) ? data.id : '';
const secret = (data && data.secret) ? 'X'.repeat(data.secret.length - 5) + data.secret.substr(-5) : '';
res.json({ google_oauth_config: {
id: id,
secret: secret,
}});
next();
},
);
router.post(
'/auth/config',
async (req, res, next) => {
await setOAuthConfiguration(req.body.data as IGoogleOAuthConfiguration);
res.send('ok');
next();
await setOAuthGood(false);
// Take this opportunity to create some database records that we'll need
await findOrCreateTagByKey(SUMMARY_SCORE_TAG);
restartService();
},
);
return router;
}
export function createAuthRouter(): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
router.get(
'/auth/test',
passport.authenticate('jwt', { session: false }),
(_, res) => {
res.send('You should not be able to see this unless you have a valid JWT token');
},
);
// Start OAuth login entrypoint. It should forward to Google OAuth servers
router.get(
'/auth/login/google',
async (req, res, next) => {
const serverCSRF = await generateServerCSRF(req, res, next);
if (res.headersSent) {
return;
}
passport.authenticate('google', {
session: false,
// Get profile information and email address
// https://developers.google.com/+/web/api/rest/oauth#authorization-scopes
scope: ['profile', 'email'],
state: serverCSRF,
// Force approval UI and account switcher in OAuth login
accessType: 'online',
prompt: 'consent',
} as any)(req, res, next);
},
);
// Complete OAuth login entrypoint. Google returns here after successful login
// We create a login token and forward to the
router.get(
'/auth/callback/google',
(req, res, next) => {
passport.authenticate('google', {
session: false,
}, async (err: any, user: User | false, info: any) => {
const {clientCSRF, referrer, errorMessage} = await getClientCSRF(req);
if (err) {
return redirectToFrontend(
res,
false,
{
errorMessage: `Authentication error: ${err.toString()}`,
},
);
}
if (!user) {
return redirectToFrontend(
res,
false,
{
errorMessage: info.reason,
},
);
}
if (errorMessage) {
return redirectToFrontend(
res,
false,
{
errorMessage: errorMessage,
},
);
}
const token = await createToken(user.id, user.get('email'));
return redirectToFrontend(
res,
true,
{
token,
csrf: clientCSRF,
},
referrer,
);
})(req, res, next);
},
);
return router;
}
================================================
FILE: packages/backend-api/src/auth/tokens.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomBytes } from 'crypto';
import * as jwt from 'jsonwebtoken';
import { isNumber } from 'lodash';
import * as moment from 'moment';
import {
CONFIGURATION_TOKEN,
getConfigItem,
setConfigItem,
User,
} from '../models';
export interface ITokenConfiguration {
secret: string;
issuer: string;
expiration_minutes: number;
}
let config: ITokenConfiguration | null;
export async function getTokenConfiguration(): Promise {
if (config) {
return config;
}
config = await getConfigItem(CONFIGURATION_TOKEN) as ITokenConfiguration;
if (config) {
return config;
}
config = await new Promise((resolve, reject) => {
randomBytes(48, (err, buffer) => {
if (err) {
reject(err);
}
resolve({
secret: buffer.toString('base64'),
issuer: 'OSMod',
expiration_minutes: 12 * 60,
});
});
});
await setConfigItem(CONFIGURATION_TOKEN, config);
return config;
}
export interface ITokenPayload {
iat: number;
user: number;
email?: string;
}
export function isValidToken(tokenPayload: ITokenPayload): boolean {
return !(!isNumber(tokenPayload.user) || tokenPayload.user < 1);
}
/**
* Indicate whether token iat (issue at timestamp) is before our configured
* threshold of days
*
* @param {object} user User model instance
* @param {object} tokenPayload Decoded token payload object with an `iat` key
* @return {boolean}
*/
export async function isExpired(user: User, tokenPayload: ITokenPayload): Promise {
if (user.group === 'service') {
return false;
}
const c = await getTokenConfiguration();
const cutoff = moment().subtract(c.expiration_minutes, 'minutes').unix();
return tokenPayload.iat < cutoff;
}
/**
* Create a JWT token for the passed in User model instance or user id
*
* @param userId User's ID
* @param email: User's email address
* @return JWT token string
*/
export async function createToken(userId: number, email?: string): Promise {
const c = await getTokenConfiguration();
return jwt.sign({
user: userId,
email,
},
c.secret,
{
issuer: c.issuer,
});
}
/**
* Verify a JWT token. Returns false for an invalid or expired token
* otherwise returns the decoded token
*
* @param {string} token JWT token to verify
* @return If token is valid, return decoded token data
*/
export async function verifyToken(token: string): Promise {
const c = await getTokenConfiguration();
try {
const decoded = jwt.verify(token, c.secret) as ITokenPayload;
if (isValidToken(decoded)) {
return decoded;
}
return null;
}
catch (err) {
return null;
}
}
/**
* Checks validity of passed in token and returns a fresh one if it passes
*
* @param {string} token JWT token to decode and refresh
*/
export async function refreshToken(token: string): Promise {
const verified = await verifyToken(token);
if (verified) {
return createToken(verified.user, verified.email);
}
return null;
}
================================================
FILE: packages/backend-api/src/auth/users.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
User,
USER_GROUP_ADMIN,
USER_GROUP_YOUTUBE,
UserSocialAuth,
} from '../models';
/**
* Indicates whether a user is valid to be authenticated
*
* @param {object} user User model instance
*/
export function isValidUser(user: User): boolean {
return user.isActive;
}
/**
* Find or create user social auth based on passed in data
*
* @param {object} user User model instance to associate with
* @param {object} data Object of data formatted for UserSocialAuth model
* @return {object} Promise object that resolves to `instance` (UserSocialAuth instance) and
* `created` (boolean) (use .spread())
*/
export async function findOrCreateUserSocialAuth(
user: User,
data: Pick,
): Promise<[UserSocialAuth, boolean]> {
const socialAuthData = {
...data,
userId: user.id,
};
const [userSocialAuth, created] = await UserSocialAuth.findOrCreate({
where: {
userId: socialAuthData.userId,
provider: socialAuthData.provider,
socialId: socialAuthData.socialId,
},
defaults: socialAuthData,
});
return [userSocialAuth, created];
}
export async function isFirstUserInitialised() {
const count = await User.count({where: {group: USER_GROUP_ADMIN, isActive: true}});
return count > 0;
}
export async function ensureFirstUser({name, email}: {name: string, email: string}) {
if (await isFirstUserInitialised()) {
return;
}
const [user, created] = await User.findOrCreate({
where: {email: email},
defaults: {
name: name,
group: USER_GROUP_ADMIN,
isActive: true,
},
});
if (!created) {
// We are repurposing an existing user. So ensure they have the correct properties
if (!await user.isActive) {
user.isActive = true;
await user.save();
}
if (await user.group !== USER_GROUP_ADMIN) {
user.group = USER_GROUP_ADMIN;
await user.save();
}
}
return user;
}
export async function saveYouTubeUserToken({name, email}: {name: string, email: string}, token: any) {
const [user, created] = await User.findOrCreate({
where: {email: email, group: USER_GROUP_YOUTUBE},
defaults: {
name: name,
group: USER_GROUP_YOUTUBE,
isActive: true,
},
});
if (!created) {
if (!await user.isActive) {
user.isActive = true;
}
}
user.extra = {token: token};
await user.save();
}
================================================
FILE: packages/backend-api/src/auth/utils.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import * as moment from 'moment';
import { generate } from 'randomstring';
import { CSRF } from '../models';
export async function generateServerCSRF(req: express.Request, res: express.Response, next: express.NextFunction) {
const clientCSRF = req.query.csrf;
const referrer = req.query.referrer;
if (!clientCSRF) {
res.status(403).send('No CSRF included in login request.');
next();
return;
}
const serverCSRF = generate();
await CSRF.create({
serverCSRF,
clientCSRF,
referrer,
});
return serverCSRF;
}
export async function getClientCSRF(req: express.Request):
Promise<{clientCSRF: string|undefined, referrer: string|null|undefined, errorMessage: string|undefined}> {
const serverCSRF = req.query.state as string;
if (!serverCSRF) {
return {clientCSRF: undefined, referrer: undefined, errorMessage: 'CSRF missing.'};
}
const csrf = await CSRF.findOne({
where: {serverCSRF},
});
if (!csrf) {
return {clientCSRF: undefined, referrer: undefined, errorMessage: 'CSRF not valid.'};
}
const maxAge = moment().subtract(5, 'minutes').toDate();
const age = csrf.createdAt;
const clientCSRF = csrf.clientCSRF;
const referrer = csrf.referrer;
await csrf.destroy();
if (age < maxAge) {
return {clientCSRF: undefined, referrer: referrer, errorMessage: 'CSRF from server is older than 5 minutes.'};
}
return {clientCSRF: clientCSRF, referrer: referrer, errorMessage: undefined};
}
================================================
FILE: packages/backend-api/src/auth/youtube.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as express from 'express';
import { google } from 'googleapis';
import { config } from '../config';
import { IGoogleOAuthConfiguration } from './config';
import { saveYouTubeUserToken } from './users';
import { generateServerCSRF, getClientCSRF } from './utils';
export function createYouTubeRouter(
oauthConfig: IGoogleOAuthConfiguration,
authenticator: any,
): express.Router {
const router = express.Router({
caseSensitive: true,
mergeParams: true,
});
if (authenticator) {
// Only the connect entrypoint should be authenticated.
router.get('/youtube/connect', authenticator);
}
router.get(
'/youtube/connect',
async (req, res, next) => {
const serverCSRF = await generateServerCSRF(req, res, next);
if (res.headersSent) {
return;
}
const oauth2Client = new google.auth.OAuth2(
oauthConfig.id,
oauthConfig.secret,
`${config.get('api_url')}/youtube/callback`);
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['profile', 'email', 'https://www.googleapis.com/auth/youtube.force-ssl'],
state: serverCSRF,
prompt: 'consent',
});
res.redirect(authUrl);
next();
},
);
router.get(
'/youtube/callback',
async (req, res) => {
const {clientCSRF, errorMessage} = await getClientCSRF(req);
const params: any = {
csrf: clientCSRF,
};
if (req.query.error) {
params['errorMessage'] = `Login rejected: ${req.query.error}`;
}
else if (errorMessage) {
params['errorMessage'] = errorMessage;
}
const oauth2Client = new google.auth.OAuth2(
oauthConfig.id,
oauthConfig.secret,
`${config.get('api_url')}/youtube/callback`, );
const tokenRsp = await oauth2Client.getToken(req.query.code as string);
const token = tokenRsp.tokens;
oauth2Client.setCredentials(token);
const service = google.oauth2('v2');
const uiRsp = await service.userinfo.get({auth: oauth2Client});
saveYouTubeUserToken({name: uiRsp.data.name || 'Youtube user', email: uiRsp.data.email || 'youtube@user'}, token);
const frontend_url = config.get('frontend_url');
res.redirect(`${frontend_url}/settings`);
});
return router;
}
================================================
FILE: packages/backend-api/src/commands/articles/delete.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as yargs from 'yargs';
import { denormalizeCommentCountsForCategory } from '../../domain';
import { logger } from '../../logger';
import { Article, Category } from '../../models';
export const command = 'articles:delete';
export const describe = 'Delete all articles from the database.';
export function builder(args: yargs.Argv) {
return args
.usage('Usage: node $0 articles:delete');
}
export async function handler() {
logger.info(`Deleting articles`);
try {
await Article.destroy({where: {}});
const categories = await Category.findAll();
for (const c of categories) {
logger.info('Denormalizing category ' + c.id);
denormalizeCommentCountsForCategory(c);
}
}
catch (err) {
logger.error('Delete articles error: ', err.name, err.message);
logger.error(err.errors);
process.exit(1);
}
logger.info('Articles successfully deleted');
process.exit(0);
}
================================================
FILE: packages/backend-api/src/commands/comments/calculate_text_size.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as yargs from 'yargs';
import { calculateTextSize } from '../../domain';
import { logger } from '../../logger';
import { Comment } from '../../models';
export const command = 'comments:calculate-text-size';
export const describe = 'Using node-canvas, calculate a single comment height at a given width.';
export function builder(args: yargs.Argv) {
return args
.usage('Usage: node $0 comments:calculate-text-size')
.demand('comment-id')
.number('comment-id')
.describe('comment-id', 'The comment id')
.demand('width')
.number('width')
.describe('width', 'The text width');
}
export async function handler(argv: any) {
const width = argv.width;
const commentId = argv.commentId;
logger.info(`Calculating comment (${commentId}) text size at ${width}`);
try {
const comment = await Comment.findByPk(commentId, {
attributes: ['id', 'text'],
});
if (!comment) {
logger.error(`No such comment: ${commentId}`);
return;
}
const height = await calculateTextSize(comment, width);
console.log(`Height in pixels`, height);
} catch (err) {
logger.error('Calculate comment text size error: ', err.name, err.message);
logger.error(err.errors);
process.exit(1);
}
process.exit(0);
}
================================================
FILE: packages/backend-api/src/commands/comments/data_helpers.ts
================================================
import { logger } from '../../logger';
import {
Article,
Category,
Comment,
IAuthorAttributes,
RESET_COUNTS,
User,
USER_GROUP_SERVICE,
} from '../../models';
import { postProcessComment, sendForScoring } from '../../pipeline';
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}
export async function createOwner(name: string) {
const [owner, ] = await User.findOrCreate({
where: {name: 'alice service user'},
defaults: {
name: name,
group: USER_GROUP_SERVICE,
isActive: true,
},
});
return owner;
}
export async function createCategory(owner: User | null, label: string) {
const [category, created] = await Category.findOrCreate({
where: {label},
defaults: {
ownerId: owner?.id,
label,
sourceId: guid(),
...RESET_COUNTS,
},
});
if (created) {
logger.info(`Generated category ${category.id}: ${category.label}`);
}
return category;
}
export async function createArticle(
category: Category,
title: string,
text: string,
url: string,
) {
const [article, created] = await Article.findOrCreate({
where: {title},
defaults: {
categoryId: category.id,
ownerId: category.ownerId,
sourceId: guid(),
title,
text,
url,
sourceCreatedAt: new Date(Date.now()),
isCommentingEnabled: true,
isAutoModerated: true,
...RESET_COUNTS,
},
});
if (created) {
logger.info(`Created article ${article.id}: ${article.title}`);
}
return article;
}
export async function createComment(
article: Article,
authorName: string,
text: string,
) {
const author: IAuthorAttributes = {
name: authorName,
};
const comment = await Comment.create({
articleId: article.id,
ownerId: article.ownerId,
sourceId: guid(),
sourceCreatedAt: new Date(Date.now()),
authorSourceId: guid(),
author,
text,
});
await postProcessComment(comment);
await sendForScoring(comment);
return comment;
}
================================================
FILE: packages/backend-api/src/commands/comments/delete.ts
================================================
/*
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as yargs from 'yargs';
import { denormalizeCommentCountsForArticle } from '../../domain';
import { logger } from '../../logger';
import { Article, Comment } from '../../models';
export const command = 'comments:delete';
export const describe = 'Delete all comments from the database.';
export function builder(args: yargs.Argv) {
return args
.usage('Usage: node $0 comments:delete');
}
export async function handler() {
logger.info(`Deleting comments`);
try {
await Comment.destroy({where: {}});
const articles = await Article.findAll();
for (const a of articles) {
logger.info('Denormalizing article ' + a.id);
denormalizeCommentCountsForArticle(a, false);
}
}
catch (err) {
logger.error('Delete comments error: ', err.name, err.message);
logger.error(err.errors);
process.exit(1);
}
logger.info('Comments successfully deleted');
process.exit(0);
}
================================================
FILE: packages/backend-api/src/commands/comments/flag.ts
================================================
/*
Copyright 2019 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Op } from 'sequelize';
import * as yargs from 'yargs';
import { denormalizeCommentCountsForArticle, denormalizeCountsForComment } from '../../domain';
import { Comment, CommentFlag } from '../../models';
export const command = 'comments:flag';
export const describe = 'Flag comments.';
export function builder(args: yargs.Argv) {
return args
.usage('Usage: node $0 comments:flag --label