<commit_before>import React, { Fragment, Component } from 'react'
import styled, { css, keyframes } from 'styled-components'
import {
  Box,
  Container,
  Flex,
  Heading,
  Image,
  Link,
  Section,
  Text,
  theme,
  cx
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Nav from 'components/Nav'
import Footer from 'components/Footer'
import Name from 'components/Name'
import Sheet from 'components/Sheet'
import IconButton from 'components/IconButton'
import Help from 'components/challenge/Help'
import Form from 'components/challenge/Form'
import Ended from 'components/challenge/Ended'
import Story from 'components/challenge/Story'
import Santa from 'components/challenge/Santa'
import Posts from 'components/challenge/Posts'
import DiscussChallenge from 'components/challenge/DiscussChallenge'
import {
  DropdownContainer,
  DropdownMenu,
  DropdownMenuOption
} from 'components/Dropdown'
import { dt, tinyDt } from 'helpers'
import { isEmpty, keys } from 'lodash'
import api from 'api'
import storage from 'storage'

const sortByHumanized = {
  trending: 'Trending',
  top: 'Top-voted',
  newest: 'Newest',
  viewed: 'Top-viewed',
  random: 'Random'
}

const pulse = keyframes`
  0% { background-color: ${cx('blue.5')}; }
  33% { background-color: ${cx('fuschia.5')}; }
  66% { background-color: ${cx('violet.5')}; }
  100% { background-color: ${cx('blue.5')}; }
`
const Header = styled(Section.withComponent('header'))`
  background-color: ${theme.colors.blue[5]};
  background-image: linear-gradient(
    90deg,
    rgba(255, 0, 0, 1),
    rgba(0, 255, 255, 1)
  );
  background-blend-mode: overlay;
  animation: ${pulse} 16s linear infinite;
`

const HeaderContainer = styled(Container)`
  display: grid;
  grid-gap: ${theme.space[4]}px;
  grid-template-areas: 'text' 'info' 'form';
  ${theme.mediaQueries.md} {
    grid-template-columns: repeat(2, 1fr);
    ${props =>
      props.success
        ? css`
            grid-template-areas:
              'text form'
              'info form';
            ${HeaderAreaText} {
              text-align: right;
              h1 {
                margin-right: -${theme.space[2]}px;
              }
            }
          `
        : css`
            grid-template-areas:
              'text text'
              'info form';
          `};
    }
  }
`

const HeaderCard = styled(Sheet).attrs({ p: 3, mb: 0 })`
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: left;
  h2,
  p {
    color: ${theme.colors.black} !important;
  }
  ${props =>
    props.status === 'needsToAuth' &&
    css`
      input[type='submit'] {
        color: ${theme.colors.info} !important;
      }
    `};
`

const HeaderAreaText = styled(Box)`
  grid-area: text;
`
const HeaderAreaInfo = styled(HeaderCard)`
  grid-area: info;
  p {
    line-height: 1.375;
    margin-bottom: ${theme.space[2]}px;
  }
`
const HeaderAreaForm = styled(HeaderCard)`
  grid-area: form;
`

const SubmissionsHeading = styled(Flex)`
  border-bottom: 1px solid ${theme.colors.smoke};
`

const title = 'Hack Club Challenge – Holiday 2018'
const desc =
  'Join Hack Club’s high school coding challenge. Submit your entry to compete in our monthly programming contest and win prizes, sponsored by Sourcegraph.'
const img = 'https://hackclub.com/cards/challenge.png'

export default class extends Component {
  state = { status: 'loading', sortBy: 'top' }

  componentDidMount() {
    if (storage.get('authToken')) {
      api
        .get(`v1/users/current`)
        .then(user => {
          this.setState({ status: 'success', userId: user.id })
        })
        .catch(err => {
          if (err.status === 401) {
            this.setState({ status: 'needsToAuth' })
          } else {
            this.setState({ status: 'error' })
          }
        })
    } else {
      this.setState({ status: 'needsToAuth' })
    }
  }

  render() {
    const { data } = this.props
    const { userId, status, sortBy } = this.state
    if (isEmpty(data)) return null
    const challenge = data.allChallengesJson.edges[0].node
    const ended = Date.parse(new Date()) > Date.parse(challenge.end)
    return (
      <Fragment>
        <Helmet
          title={title}
          meta={[
            { name: 'description', content: desc },
            { name: 'twitter:title', content: title },
            { name: 'twitter:description', content: desc },
            { name: 'twitter:image', content: img },
            { property: 'og:title', content: title },
            { property: 'og:description', content: desc },
            { property: 'og:image', content: img },
            { property: 'og:url', content: 'https://hackclub.com/challenge' }
          ]}
        />

          <HeaderContainer
            maxWidth={64}
            pt={[4, 5, 6]}
            pb={4}
            align="left"
            success={status === 'success'}
          >
            <HeaderAreaText align="center" pt={[4, 3]}>
              <Name fontSize={6}>Challenge</Name>
              <Flex
                my={3}
                align="center"
                justify={
                  status === 'success' ? ['center', 'flex-end'] : 'center'
                }
              >
                <Link href="https://sourcegraph.com" target="_blank">
                  <Image
                    alt="Sourcegraph logo"
                    src="/sourcegraph-light.svg"
                    width={[144, 224]}
                  />
                </Link>
                <Text.span
                  color="rgba(255, 255, 255, 0.875)"
                  fontSize={4}
                  mx={[2, 3]}
                  children="+"
                />
                <Text.span
                  color="white"
                  fontSize={[3, 4]}
                  bold
                  caps
                  children="Hack Club"
                />
              </Flex>
            </HeaderAreaText>
            <HeaderAreaInfo>
              <Text fontSize={2}>
                🌟 {challenge.name}
              </Text>
              <Text fontSize={2}>🎁 {challenge.description}</Text>
              <Text fontSize={2}>
                ℹ Competition open to Hack Club community members
              </Text>
              <Text fontSize={2}>
                🏅 Submissions due {dt(challenge.end)}. Top 3 voted win!
              </Text>
            </HeaderAreaInfo>
            <HeaderAreaForm status={status}>
              <Help />
              <Form challengeId={challenge.id} status={status} />
            </HeaderAreaForm>
          </HeaderContainer>
          <Story />

        <Container maxWidth={48} pt={4} pb={5} px={[0, 3]}>
          {ended ? <Ended /> : <Santa />}
          <SubmissionsHeading align="center" pb={2} pl={[2, 0]} pr={[2, 3]}>
            <Flex align="center" flex="1 1 auto" wrap>
              <Heading.h2 color="black" fontSize={5} mr={2}>
                Submissions
              </Heading.h2>
              <Text fontSize={2} color="muted" align="left">
                {tinyDt(challenge.start)}–{tinyDt(challenge.end)}
              </Text>
            </Flex>
            <DropdownContainer>
              <IconButton
                glyph="filter"
                bg="info"
                fontSize={2}
                style={{ whiteSpace: 'nowrap' }}
                children={sortByHumanized[sortBy]}
              />
              <DropdownMenu>
                {keys(sortByHumanized).map(key => (
                  <DropdownMenuOption
                    active={sortBy === key}
                    onClick={() => this.setState({ sortBy: key })}
                    children={sortByHumanized[key]}
                    key={key}
                  />
                ))}
              </DropdownMenu>
            </DropdownContainer>
          </SubmissionsHeading>
          <Posts
            challengeId={challenge.id}
            userId={userId}
            status={status}
            sortBy={sortBy}
          />
          <DiscussChallenge />
        </Container>

      </Fragment>
    )
  }
}
export const pageQuery = graphql`
  query ChallengeQuery {
    allChallengesJson(
      filter: { id: { eq: "7" } }
      sort: { fields: [start], order: DESC }
      limit: 1
    ) {
      edges {
        node {
          id
          name
          description
          start
          end
        }
      }
    }
  }
`
<commit_msg>Shoutout<commit_after>import React, { Fragment, Component } from 'react'
import styled, { css, keyframes } from 'styled-components'
import {
  Box,
  Container,
  Flex,
  Heading,
  Image,
  Link,
  Section,
  Text,
  theme,
  cx
} from '@hackclub/design-system'
import Helmet from 'react-helmet'
import Nav from 'components/Nav'
import Footer from 'components/Footer'
import Name from 'components/Name'
import Sheet from 'components/Sheet'
import IconButton from 'components/IconButton'
import Help from 'components/challenge/Help'
import Form from 'components/challenge/Form'
import Ended from 'components/challenge/Ended'
import Story from 'components/challenge/Story'
import Santa from 'components/challenge/Santa'
import Posts from 'components/challenge/Posts'
import DiscussChallenge from 'components/challenge/DiscussChallenge'
import {
  DropdownContainer,
  DropdownMenu,
  DropdownMenuOption
} from 'components/Dropdown'
import { dt, tinyDt } from 'helpers'
import { isEmpty, keys } from 'lodash'
import api from 'api'
import storage from 'storage'

const sortByHumanized = {
  trending: 'Trending',
  top: 'Top-voted',
  newest: 'Newest',
  viewed: 'Top-viewed',
  random: 'Random'
}

const pulse = keyframes`
  0% { background-color: ${cx('blue.5')}; }
  33% { background-color: ${cx('fuschia.5')}; }
  66% { background-color: ${cx('violet.5')}; }
  100% { background-color: ${cx('blue.5')}; }
`
const Header = styled(Section.withComponent('header'))`
  background-color: ${theme.colors.blue[5]};
  background-image: linear-gradient(
    90deg,
    rgba(255, 0, 0, 1),
    rgba(0, 255, 255, 1)
  );
  background-blend-mode: overlay;
  animation: ${pulse} 16s linear infinite;
`

const HeaderContainer = styled(Container)`
  display: grid;
  grid-gap: ${theme.space[4]}px;
  grid-template-areas: 'text' 'info' 'form';
  ${theme.mediaQueries.md} {
    grid-template-columns: repeat(2, 1fr);
    ${props =>
      props.success
        ? css`
            grid-template-areas:
              'text form'
              'info form';
            ${HeaderAreaText} {
              text-align: right;
              h1 {
                margin-right: -${theme.space[2]}px;
              }
            }
          `
        : css`
            grid-template-areas:
              'text text'
              'info form';
          `};
    }
  }
`

const HeaderCard = styled(Sheet).attrs({ p: 3, mb: 0 })`
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: left;
  h2,
  p {
    color: ${theme.colors.black} !important;
  }
  ${props =>
    props.status === 'needsToAuth' &&
    css`
      input[type='submit'] {
        color: ${theme.colors.info} !important;
      }
    `};
`

const HeaderAreaText = styled(Box)`
  grid-area: text;
`
const HeaderAreaInfo = styled(HeaderCard)`
  grid-area: info;
  p {
    line-height: 1.375;
    margin-bottom: ${theme.space[2]}px;
  }
`
const HeaderAreaForm = styled(HeaderCard)`
  grid-area: form;
`

const SubmissionsHeading = styled(Flex)`
  border-bottom: 1px solid ${theme.colors.smoke};
`

const title = 'Hack Club Challenge – Holiday 2018'
const desc =
  'Join Hack Club’s high school coding challenge. Submit your entry to compete in our monthly programming contest and win prizes, sponsored by Sourcegraph.'
const img = 'https://hackclub.com/cards/challenge.png'

export default class extends Component {
  state = { status: 'loading', sortBy: 'top' }

  componentDidMount() {
    if (storage.get('authToken')) {
      api
        .get(`v1/users/current`)
        .then(user => {
          this.setState({ status: 'success', userId: user.id })
        })
        .catch(err => {
          if (err.status === 401) {
            this.setState({ status: 'needsToAuth' })
          } else {
            this.setState({ status: 'error' })
          }
        })
    } else {
      this.setState({ status: 'needsToAuth' })
    }
  }

  render() {
    const { data } = this.props
    const { userId, status, sortBy } = this.state
    if (isEmpty(data)) return null
    const challenge = data.allChallengesJson.edges[0].node
    const ended = Date.parse(new Date()) > Date.parse(challenge.end)
    return (
      <Fragment>
        <Helmet
          title={title}
          meta={[
            { name: 'description', content: desc },
            { name: 'twitter:title', content: title },
            { name: 'twitter:description', content: desc },
            { name: 'twitter:image', content: img },
            { property: 'og:title', content: title },
            { property: 'og:description', content: desc },
            { property: 'og:image', content: img },
            { property: 'og:url', content: 'https://hackclub.com/challenge' }
          ]}
        />

          <HeaderContainer
            maxWidth={64}
            pt={[4, 5, 6]}
            pb={4}
            align="left"
            success={status === 'success'}
          >
            <HeaderAreaText align="center" pt={[4, 3]}>
              <Name fontSize={6}>Challenge</Name>
              <Flex
                my={3}
                align="center"
                justify={
                  status === 'success' ? ['center', 'flex-end'] : 'center'
                }
              >
                <Link href="https://sourcegraph.com" target="_blank">
                  <Image
                    alt="Sourcegraph logo"
                    src="/sourcegraph-light.svg"
                    width={[144, 224]}
                  />
                </Link>
                <Text.span
                  color="rgba(255, 255, 255, 0.875)"
                  fontSize={4}
                  mx={[2, 3]}
                  children="+"
                />
                <Text.span
                  color="white"
                  fontSize={[3, 4]}
                  bold
                  caps
                  children="Hack Club"
                />
              </Flex>
            </HeaderAreaText>
            <HeaderAreaInfo>
              <Text fontSize={2}>
                🌟 {challenge.name}
              </Text>
              <Text fontSize={2}>
                🎁 {challenge.description} (thank you <Link href="https://sourcegraph.com" target="_blank">Sourcegraph</Link>!)
              </Text>
              <Text fontSize={2}>
                ℹ Competition open to Hack Club community members
              </Text>
              <Text fontSize={2}>
                🏅 Submissions due {dt(challenge.end)}. Top 3 voted win!
              </Text>
            </HeaderAreaInfo>
            <HeaderAreaForm status={status}>
              <Help />
              <Form challengeId={challenge.id} status={status} />
            </HeaderAreaForm>
          </HeaderContainer>
          <Story />

        <Container maxWidth={48} pt={4} pb={5} px={[0, 3]}>
          {ended ? <Ended /> : <Santa />}
          <SubmissionsHeading align="center" pb={2} pl={[2, 0]} pr={[2, 3]}>
            <Flex align="center" flex="1 1 auto" wrap>
              <Heading.h2 color="black" fontSize={5} mr={2}>
                Submissions
              </Heading.h2>
              <Text fontSize={2} color="muted" align="left">
                {tinyDt(challenge.start)}–{tinyDt(challenge.end)}
              </Text>
            </Flex>
            <DropdownContainer>
              <IconButton
                glyph="filter"
                bg="info"
                fontSize={2}
                style={{ whiteSpace: 'nowrap' }}
                children={sortByHumanized[sortBy]}
              />
              <DropdownMenu>
                {keys(sortByHumanized).map(key => (
                  <DropdownMenuOption
                    active={sortBy === key}
                    onClick={() => this.setState({ sortBy: key })}
                    children={sortByHumanized[key]}
                    key={key}
                  />
                ))}
              </DropdownMenu>
            </DropdownContainer>
          </SubmissionsHeading>
          <Posts
            challengeId={challenge.id}
            userId={userId}
            status={status}
            sortBy={sortBy}
          />
          <DiscussChallenge />
        </Container>

      </Fragment>
    )
  }
}
export const pageQuery = graphql`
  query ChallengeQuery {
    allChallengesJson(
      filter: { id: { eq: "7" } }
      sort: { fields: [start], order: DESC }
      limit: 1
    ) {
      edges {
        node {
          id
          name
          description
          start
          end
        }
      }
    }
  }
`
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import Helmet from '../components/helmet'
import Link from 'gatsby-link'
import styled from 'emotion/react'

import { PageContainer } from '../layouts/containers'
import SectionItemList from '../components/SectionItemList'
import { chooseDesignerImage, designerLink, byLastName } from '../util'

const prefferedStatusOrder = ['Represented', 'Available']

export default function Designers({ data }) {
  const { allDesignersYaml } = data

  // Technically this can be `const` but that's a lie right ;p
  let designers = allDesignersYaml.edges.map(edge => edge.node)
  designers.sort(byLastName)

  const designersByStatus = {}
  designers.forEach(d => {
    if (!designersByStatus[d.status]) {
      designersByStatus[d.status] = [d]
    } else {
      designersByStatus[d.status].push(d)
    }
  })

  const sortedStatuses = Object.keys(designersByStatus).sort(
    (a, b) => prefferedStatusOrder.indexOf(a) - prefferedStatusOrder.indexOf(b)
  )

  const listSections = sortedStatuses.map(status => {
    const items = designersByStatus[status].map(designer => ({
      title: designer.title,
      image: chooseDesignerImage(designer),
      link: designerLink(designer.slug),
    }))

    const title = status === 'Represented' ? null : 'Also Available'

    return { title, items }
  })

  return (
    <PageContainer>
      <Helmet
        title={`Salon 94 Design - Designers`}
        description={`List of designers represented by Salon 94 Design.`}
      />

        <SectionItemList title="Designers" sections={listSections} />

    </PageContainer>
  )
}

// I include the gql fragments in the index just to colocate them
export const pageQuery = graphql`
  query DesignersQuery {
    allDesignersYaml(sort: { order: ASC, fields: [title] }) {
      edges {
        node {
          slug
          title
          status
          ...baseWorkFields
        }
      }
    }
  }
`
<commit_msg>remove comment<commit_after>import React, { Component } from 'react'
import Helmet from '../components/helmet'
import Link from 'gatsby-link'
import styled from 'emotion/react'

import { PageContainer } from '../layouts/containers'
import SectionItemList from '../components/SectionItemList'
import { chooseDesignerImage, designerLink, byLastName } from '../util'

const prefferedStatusOrder = ['Represented', 'Available']

export default function Designers({ data }) {
  const { allDesignersYaml } = data

  // Technically this can be `const` but that's a lie right ;p
  let designers = allDesignersYaml.edges.map(edge => edge.node)
  designers.sort(byLastName)

  const designersByStatus = {}
  designers.forEach(d => {
    if (!designersByStatus[d.status]) {
      designersByStatus[d.status] = [d]
    } else {
      designersByStatus[d.status].push(d)
    }
  })

  const sortedStatuses = Object.keys(designersByStatus).sort(
    (a, b) => prefferedStatusOrder.indexOf(a) - prefferedStatusOrder.indexOf(b)
  )

  const listSections = sortedStatuses.map(status => {
    const items = designersByStatus[status].map(designer => ({
      title: designer.title,
      image: chooseDesignerImage(designer),
      link: designerLink(designer.slug),
    }))

    const title = status === 'Represented' ? null : 'Also Available'

    return { title, items }
  })

  return (
    <PageContainer>
      <Helmet
        title={`Salon 94 Design - Designers`}
        description={`List of designers represented by Salon 94 Design.`}
      />

        <SectionItemList title="Designers" sections={listSections} />

    </PageContainer>
  )
}

export const pageQuery = graphql`
  query DesignersQuery {
    allDesignersYaml(sort: { order: ASC, fields: [title] }) {
      edges {
        node {
          slug
          title
          status
          ...baseWorkFields
        }
      }
    }
  }
`
<|endoftext|>


<commit_before>'use strict';

const { addAllErrorHandlers } = require('./error');

const addOperator = {
  check (opVal) {
    if (opVal !== 2) { return; }

    return 'value cannot be equal to 2';
  },

  attribute: ['string'],

  argument: ['string', 'integer[]', 'boolean[]', 'null[]'],
};

// All patch operators
const operators = {
  _add: addOperator,
};

const OPERATORS = addAllErrorHandlers({ operators });

module.exports = {
  OPERATORS,
};
<commit_msg>Improve _add operator<commit_after>'use strict';

const { addAllErrorHandlers } = require('./error');

const addOperator = {
  check (opVal) {
    if (opVal !== 2) { return; }

    return 'value cannot be equal to 2';
  },

  attribute: ['integer', 'number'],

  argument: ['integer', 'number', 'null'],

  apply (attrVal, opVal) {
    const opValA = opVal == null ? 0 : opVal;
    return attrVal + opValA;
  },
};

// All patch operators
const operators = {
  _add: addOperator,
};

const OPERATORS = addAllErrorHandlers({ operators });

module.exports = {
  OPERATORS,
};
<|endoftext|>


<commit_before>// The MIT License (MIT)

// Copyright (c) 2015 Tanay PrabhuDesai

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

var connString = process.env.DB_CONN_STRING || 'mongodb://127.0.0.1:27017/voix-twitter';
var mongo = require('mongodb');
var TwitterController = require('./twitter_controller');
var db = require('monk')(connString);

function PollController(){
  this.db = db.get('poll');
  this.twitter_db = db.get('twitter');
  this.votes = {};
  this.poll_id = null;
  this.doc= null;
  this.twitter = new TwitterController();
}

PollController.prototype.assignDummy = function () {
  var parent = this;
  this.poll_id = '563f23812399ec75d216e9d2';
}

PollController.prototype.createPoll = function (obj,callback) {
  var parent = this;
  // parent.db.insert(obj,)
};

var unifyVoteKeywords = function (doc) {
  var res = [];
  for (var i = 0; i < doc.votes.length; i++) {
    for (var j = 0; j < doc.votes[i].tags.length; j++) {
      res.push(doc.votes[i].tags[j]);
    }
  }
  console.log(JSON.stringify(res));
  return res;
};

PollController.prototype.processTweet = function (tweet_id,tweet) {
  var parent = this;
  tweet = tweet.toLowerCase();
  console.log("Processing tweet");
  parent.twitter_db.findById({id:tweet_id},function(err,doc) {
    if (err) throw err;
    console.log(JSON.stringify(doc))
    if (!doc) {    // skip redundant tweets
      // parent.votes = doc.votes;
      for (var i = 0; i < parent.votes.length; i++) {
        for (var j = 0; j < parent.votes[i].tags.length; j++) {
          if ( tweet.indexOf(parent.votes[i].tags[j].toLowerCase()) > 0 ) {
            // parent.db.updateById(parent.poll_id,{$inc:{name.count}})
            console.log("Match found");
            parent.db.update({_id:parent.poll_id,"votes.name":parent.votes[i].name,$nin:{tweets:tweet_id}},{$inc:{"votes.$.count":1},$push:{tweets:tweet_id}},function(err,doc) {
              if (err) throw err;
              if (doc) {
                console.log("Updated/incremented");
                return;
              }
            });
          }
        }
      }
    }
  });
};

PollController.prototype.fetchTweets = function(poll_id,callback) {
  var parent = this;
  console.log('called fetchign tweets');
  parent.db.findById(parent.poll_id,function(err,doc) {
    if (err) throw err;
    console.log('found poll, i guess :-|');
    if ( typeof(doc) !== 'undefined' ) {
      var dateSince = doc.start_time;
      // parent.doc = doc;
      parent.votes = doc.votes;
      parent.twitter.getTweets(doc.poll_keywords, unifyVoteKeywords(doc), doc.start_time, doc.end_time, function (ret) {
        console.log('Tweets:');
        // console.log(JSON.stringify(ret));

        for (var i = 0 ; i < ret.statuses.length ; ++i) {
          parent.processTweet(ret.statuses[i].id,ret.statuses[i].text);
          // console.log('User: @'+ret.statuses[i].user.screen_name+'   Tweet: '+ret.statuses[i].text+'   id:'+ret.statuses[i].id+'   timestamp'+ret.statuses[i].created_at);
        }

        parent.twitter.storeTweets(function() {
          callback(ret);
        })
      });
    } else {
      console.log("Error could not fetch tweets");
    }
  });
};

module.exports = PollController;
<commit_msg>Fixed redundant tweets bug<commit_after>// The MIT License (MIT)

// Copyright (c) 2015 Tanay PrabhuDesai

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

var connString = process.env.DB_CONN_STRING || 'mongodb://127.0.0.1:27017/voix-twitter';
var mongo = require('mongodb');
var TwitterController = require('./twitter_controller');
var db = require('monk')(connString);

function PollController(){
  this.db = db.get('poll');
  this.twitter_db = db.get('twitter');
  this.votes = {};
  this.poll_id = null;
  this.doc= null;
  this.twitter = new TwitterController();
}

PollController.prototype.assignDummy = function () {
  var parent = this;
  this.poll_id = '563f23812399ec75d216e9d2';
}

PollController.prototype.createPoll = function (obj,callback) {
  var parent = this;
  // parent.db.insert(obj,)
};

var unifyVoteKeywords = function (doc) {
  var res = [];
  for (var i = 0; i < doc.votes.length; i++) {
    for (var j = 0; j < doc.votes[i].tags.length; j++) {
      res.push(doc.votes[i].tags[j]);
    }
  }
  console.log(JSON.stringify(res));
  return res;
};

PollController.prototype.processTweet = function (tweet_id,tweet) {
  var parent = this;
  tweet = tweet.toLowerCase();
  console.log("Processing tweet");
  parent.twitter_db.findById({id:tweet_id},function(err,doc) {
    if (err) throw err;
    console.log(JSON.stringify(doc))
    if (!doc) {    // skip redundant tweets
      // parent.votes = doc.votes;
      for (var i = 0; i < parent.votes.length; i++) {
        for (var j = 0; j < parent.votes[i].tags.length; j++) {
          if ( tweet.indexOf(parent.votes[i].tags[j].toLowerCase()) > 0 ) {
            // parent.db.updateById(parent.poll_id,{$inc:{name.count}})
            console.log("Match found");
            parent.db.update({_id:parent.poll_id,"votes.name":parent.votes[i].name,tweets:{$not:{$eq:tweet_id}}},{$inc:{"votes.$.count":1},$push:{tweets:tweet_id}}, function(err,doc) {
              if (err) throw err;
              if (doc) {
                console.log("Updated/incremented");
                return;
              }
            });
          }
        }
      }
    }
  });
};

PollController.prototype.fetchTweets = function(poll_id,callback) {
  var parent = this;
  console.log('called fetchign tweets');
  parent.db.findById(parent.poll_id,function(err,doc) {
    if (err) throw err;
    console.log('found poll, i guess :-|');
    if ( typeof(doc) !== 'undefined' ) {
      var dateSince = doc.start_time;
      // parent.doc = doc;
      parent.votes = doc.votes;
      parent.twitter.getTweets(doc.poll_keywords, unifyVoteKeywords(doc), doc.start_time, doc.end_time, function (ret) {
        console.log('Tweets:');
        // console.log(JSON.stringify(ret));

        for (var i = 0 ; i < ret.statuses.length ; ++i) {
          parent.processTweet(ret.statuses[i].id,ret.statuses[i].text);
          // console.log('User: @'+ret.statuses[i].user.screen_name+'   Tweet: '+ret.statuses[i].text+'   id:'+ret.statuses[i].id+'   timestamp'+ret.statuses[i].created_at);
        }

        parent.twitter.storeTweets(function() {
          callback(ret);
        })
      });
    } else {
      console.log("Error could not fetch tweets");
    }
  });
};

module.exports = PollController;
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import qs from 'query-string'

import extractTimeFiltersFromQuery from 'src/util/nlp-time-filter'
import { remoteFunction } from 'src/util/webextensionRPC'
import { isLoggable, getPauseState } from 'src/activity-logger'
import * as blacklistI from 'src/blacklist'
import Popup from './components/Popup'
import Button from './components/Button'
import BlacklistConfirm from './components/BlacklistConfirm'
import HistoryPauser from './components/HistoryPauser'
import LinkButton from './components/LinkButton'
import SplitButton from './components/SplitButton'
import * as constants from './constants'

import { itemBtnBlacklisted } from './components/Button.css'

// Transforms URL checking results to state types
const getBlacklistButtonState = ({ loggable, blacklist }) => {
    if (!blacklist) return constants.BLACKLIST_BTN_STATE.BLACKLISTED
    if (!loggable) return constants.BLACKLIST_BTN_STATE.DISABLED
    return constants.BLACKLIST_BTN_STATE.UNLISTED
}

class PopupContainer extends Component {
    constructor(props) {
        super(props)

        this.state = {
            url: '',
            searchValue: '',
            pauseValue: 20,
            currentTabPageDocId: '',
            blacklistBtn: constants.BLACKLIST_BTN_STATE.DISABLED,
            isPaused: false,
            blacklistChoice: false,
            blacklistConfirm: false,
        }

        this.toggleLoggingPause = remoteFunction('toggleLoggingPause')
        this.deleteDocs = remoteFunction('deleteDocsByUrl')

        this.onSearchChange = this.onSearchChange.bind(this)
        this.onPauseChange = this.onPauseChange.bind(this)
        this.onSearchEnter = this.onSearchEnter.bind(this)
        this.onPauseConfirm = this.onPauseConfirm.bind(this)
    }

    async componentDidMount() {
        const [currentTab] = await browser.tabs.query({
            active: true,
            currentWindow: true,
        })

        // If we can't get the tab data, then can't init action button states
        if (!currentTab || !currentTab.url) {
            return
        }

        const updateState = newState =>
            this.setState(oldState => ({ ...oldState, ...newState }))
        const noop = f => f // Don't do anything if error; state doesn't change

        updateState({ url: currentTab.url })
        this.getInitPauseState()
            .then(updateState)
            .catch(noop)
        this.getInitBlacklistBtnState(currentTab.url)
            .then(updateState)
            .catch(noop)
    }

    async getInitPauseState() {
        return { isPaused: await getPauseState() }
    }

    async getInitBlacklistBtnState(url) {
        const blacklist = await blacklistI.fetchBlacklist()

        const result = {
            loggable: isLoggable({ url }),
            blacklist: !blacklistI.isURLBlacklisted(url, blacklist),
        }

        return { blacklistBtn: getBlacklistButtonState(result) }
    }

    onBlacklistBtnClick(domain = false) {
        const url = domain ? new URL(this.state.url).hostname : this.state.url

        return event => {
            event.preventDefault()
            blacklistI.addToBlacklist(url)
            this.setState(state => ({
                ...state,
                blacklistChoice: false,
                blacklistConfirm: true,
                blacklistBtn: constants.BLACKLIST_BTN_STATE.BLACKLISTED,
                url,
            }))
        }
    }

    onPauseConfirm(event) {
        event.preventDefault()
        const { isPaused, pauseValue } = this.state

        // Tell background script to do on extension level
        this.toggleLoggingPause(pauseValue)

        // Do local level state toggle and reset
        this.setState(state => ({
            ...state,
            isPaused: !isPaused,
            pauseValue: 20,
        }))
    }

    onPauseChange(event) {
        const pauseValue = event.target.value
        this.setState(state => ({ ...state, pauseValue }))
    }

    onSearchChange(event) {
        const searchValue = event.target.value
        this.setState(state => ({ ...state, searchValue }))
    }

    onSearchEnter(event) {
        if (event.key === 'Enter') {
            event.preventDefault()

            const queryFilters = extractTimeFiltersFromQuery(
                this.state.searchValue,
            )
            const queryParams = qs.stringify(queryFilters)

            browser.tabs.create({
                url: `${constants.OVERVIEW_URL}?${queryParams}`,
            }) // New tab with query
            window.close() // Close the popup
        }
    }

    // Hides full-popup confirm
    resetBlacklistConfirmState = () =>
        this.setState(state => ({ ...state, blacklistConfirm: false }))

    handleDeleteBlacklistData = () => {
        this.deleteDocs(this.state.url)
        this.resetBlacklistConfirmState()
    }

    setBlacklistChoice = () =>
        this.setState(state => ({ ...state, blacklistChoice: true }))

    renderBlacklistButton() {
        const { blacklistChoice, blacklistBtn } = this.state

        if (!blacklistChoice) {
            // Standard blacklist button
            return blacklistBtn ===
                constants.BLACKLIST_BTN_STATE.BLACKLISTED ? (
                <LinkButton
                    href={`${constants.OPTIONS_URL}#/blacklist`}
                    icon="block"
                    btnClass={itemBtnBlacklisted}
                >
                    This Page is Blacklisted. Undo>>
                </LinkButton>
            ) : (
                <Button
                    icon="block"
                    onClick={this.setBlacklistChoice}
                    disabled={
                        blacklistBtn === constants.BLACKLIST_BTN_STATE.DISABLED
                    }
                >
                    Blacklist Current Page
                </Button>
            )
        }

        // Domain vs URL choice button
        return (
            <SplitButton icon="block">
                <Button onClick={this.onBlacklistBtnClick(true)}>Domain</Button>
                <Button onClick={this.onBlacklistBtnClick(false)}>URL</Button>
            </SplitButton>
        )
    }

    renderPauseChoices() {
        const pauseValueToOption = (val, i) => (
            <option key={i} value={val}>
                {val === Infinity ? '∞' : val}
            </option>
        )

        return this.props.pauseValues.map(pauseValueToOption)
    }

    renderChildren() {
        const { blacklistConfirm, pauseValue, isPaused } = this.state

        if (blacklistConfirm) {
            return (
                <BlacklistConfirm
                    onConfirmClick={this.handleDeleteBlacklistData}
                    onDenyClick={this.resetBlacklistConfirmState}
                />
            )
        }
        return (

                <HistoryPauser
                    onConfirm={this.onPauseConfirm}
                    onChange={this.onPauseChange}
                    value={pauseValue}
                    isPaused={isPaused}
                >
                    {this.renderPauseChoices()}
                </HistoryPauser>
                {this.renderBlacklistButton()}

                <LinkButton
                    href={`${constants.OPTIONS_URL}#/blacklist`}
                    icon="settings"
                >
                    Settings
                </LinkButton>
                <LinkButton
                    href={`${constants.OPTIONS_URL}#/import`}
                    icon="file_download"
                >
                    Import History & Bookmarks
                </LinkButton>
                <LinkButton href={constants.FEEDBACK_URL} icon="feedback">
                    I need Help!
                </LinkButton>

        )
    }

    render() {
        const { searchValue } = this.state

        return (
            <Popup
                searchValue={searchValue}
                onSearchChange={this.onSearchChange}
                onSearchEnter={this.onSearchEnter}
            >
                {this.renderChildren()}
            </Popup>
        )
    }
}

PopupContainer.propTypes = {
    pauseValues: PropTypes.arrayOf(PropTypes.number).isRequired,
}
PopupContainer.defaultProps = {
    pauseValues: [5, 10, 20, 30, 60, 120, 180, Infinity],
}

export default PopupContainer
<commit_msg>Added Bookmark button in popup<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import qs from 'query-string'

import extractTimeFiltersFromQuery from 'src/util/nlp-time-filter'
import { remoteFunction } from 'src/util/webextensionRPC'
import { isLoggable, getPauseState } from 'src/activity-logger'
import * as blacklistI from 'src/blacklist'
import Popup from './components/Popup'
import Button from './components/Button'
import BlacklistConfirm from './components/BlacklistConfirm'
import HistoryPauser from './components/HistoryPauser'
import LinkButton from './components/LinkButton'
import SplitButton from './components/SplitButton'
import * as constants from './constants'

import { itemBtnBlacklisted } from './components/Button.css'

// Transforms URL checking results to state types
const getBlacklistButtonState = ({ loggable, blacklist }) => {
    if (!blacklist) return constants.BLACKLIST_BTN_STATE.BLACKLISTED
    if (!loggable) return constants.BLACKLIST_BTN_STATE.DISABLED
    return constants.BLACKLIST_BTN_STATE.UNLISTED
}

class PopupContainer extends Component {
    constructor(props) {
        super(props)

        this.state = {
            url: '',
            searchValue: '',
            pauseValue: 20,
            currentTabPageDocId: '',
            blacklistBtn: constants.BLACKLIST_BTN_STATE.DISABLED,
            isPaused: false,
            blacklistChoice: false,
            blacklistConfirm: false,
        }

        this.toggleLoggingPause = remoteFunction('toggleLoggingPause')
        this.deleteDocs = remoteFunction('deleteDocsByUrl')
        this.createBookmarkByExtension = remoteFunction(
            'createBookmarkByExtension',
        )

        this.onSearchChange = this.onSearchChange.bind(this)
        this.onPauseChange = this.onPauseChange.bind(this)
        this.onSearchEnter = this.onSearchEnter.bind(this)
        this.onPauseConfirm = this.onPauseConfirm.bind(this)
    }

    async componentDidMount() {
        const [currentTab] = await browser.tabs.query({
            active: true,
            currentWindow: true,
        })

        // If we can't get the tab data, then can't init action button states
        if (!currentTab || !currentTab.url) {
            return
        }

        const updateState = newState =>
            this.setState(oldState => ({ ...oldState, ...newState }))
        const noop = f => f // Don't do anything if error; state doesn't change

        updateState({ url: currentTab.url })
        this.getInitPauseState()
            .then(updateState)
            .catch(noop)
        this.getInitBlacklistBtnState(currentTab.url)
            .then(updateState)
            .catch(noop)
    }

    async getInitPauseState() {
        return { isPaused: await getPauseState() }
    }

    async getInitBlacklistBtnState(url) {
        const blacklist = await blacklistI.fetchBlacklist()

        const result = {
            loggable: isLoggable({ url }),
            blacklist: !blacklistI.isURLBlacklisted(url, blacklist),
        }

        return { blacklistBtn: getBlacklistButtonState(result) }
    }

    onBlacklistBtnClick(domain = false) {
        const url = domain ? new URL(this.state.url).hostname : this.state.url

        return event => {
            event.preventDefault()
            blacklistI.addToBlacklist(url)
            this.setState(state => ({
                ...state,
                blacklistChoice: false,
                blacklistConfirm: true,
                blacklistBtn: constants.BLACKLIST_BTN_STATE.BLACKLISTED,
                url,
            }))
        }
    }

    onPauseConfirm(event) {
        event.preventDefault()
        const { isPaused, pauseValue } = this.state

        // Tell background script to do on extension level
        this.toggleLoggingPause(pauseValue)

        // Do local level state toggle and reset
        this.setState(state => ({
            ...state,
            isPaused: !isPaused,
            pauseValue: 20,
        }))
    }

    onPauseChange(event) {
        const pauseValue = event.target.value
        this.setState(state => ({ ...state, pauseValue }))
    }

    onSearchChange(event) {
        const searchValue = event.target.value
        this.setState(state => ({ ...state, searchValue }))
    }

    onSearchEnter(event) {
        if (event.key === 'Enter') {
            event.preventDefault()

            const queryFilters = extractTimeFiltersFromQuery(
                this.state.searchValue,
            )
            const queryParams = qs.stringify(queryFilters)

            browser.tabs.create({
                url: `${constants.OVERVIEW_URL}?${queryParams}`,
            }) // New tab with query
            window.close() // Close the popup
        }
    }

    // Hides full-popup confirm
    resetBlacklistConfirmState = () =>
        this.setState(state => ({ ...state, blacklistConfirm: false }))

    handleDeleteBlacklistData = () => {
        this.deleteDocs(this.state.url)
        this.resetBlacklistConfirmState()
    }

    setBlacklistChoice = () =>
        this.setState(state => ({ ...state, blacklistChoice: true }))

    renderBlacklistButton() {
        const { blacklistChoice, blacklistBtn } = this.state

        if (!blacklistChoice) {
            // Standard blacklist button
            return blacklistBtn ===
                constants.BLACKLIST_BTN_STATE.BLACKLISTED ? (
                <LinkButton
                    href={`${constants.OPTIONS_URL}#/blacklist`}
                    icon="block"
                    btnClass={itemBtnBlacklisted}
                >
                    This Page is Blacklisted. Undo>>
                </LinkButton>
            ) : (
                <Button
                    icon="block"
                    onClick={this.setBlacklistChoice}
                    disabled={
                        blacklistBtn === constants.BLACKLIST_BTN_STATE.DISABLED
                    }
                >
                    Blacklist Current Page
                </Button>
            )
        }

        // Domain vs URL choice button
        return (
            <SplitButton icon="block">
                <Button onClick={this.onBlacklistBtnClick(true)}>Domain</Button>
                <Button onClick={this.onBlacklistBtnClick(false)}>URL</Button>
            </SplitButton>
        )
    }

    renderPauseChoices() {
        const pauseValueToOption = (val, i) => (
            <option key={i} value={val}>
                {val === Infinity ? '∞' : val}
            </option>
        )

        return this.props.pauseValues.map(pauseValueToOption)
    }

    handleAddBookmark = () => this.createBookmarkByExtension(this.state.url)

    renderChildren() {
        const { blacklistConfirm, pauseValue, isPaused } = this.state

        if (blacklistConfirm) {
            return (
                <BlacklistConfirm
                    onConfirmClick={this.handleDeleteBlacklistData}
                    onDenyClick={this.resetBlacklistConfirmState}
                />
            )
        }
        return (

                <Button onClick={this.handleAddBookmark} icon="bookmark">
                    Bookmark This Page
                </Button>
                <HistoryPauser
                    onConfirm={this.onPauseConfirm}
                    onChange={this.onPauseChange}
                    value={pauseValue}
                    isPaused={isPaused}
                >
                    {this.renderPauseChoices()}
                </HistoryPauser>
                {this.renderBlacklistButton()}

                <LinkButton
                    href={`${constants.OPTIONS_URL}#/blacklist`}
                    icon="settings"
                >
                    Settings
                </LinkButton>
                <LinkButton
                    href={`${constants.OPTIONS_URL}#/import`}
                    icon="file_download"
                >
                    Import History & Bookmarks
                </LinkButton>
                <LinkButton href={constants.FEEDBACK_URL} icon="feedback">
                    I need Help!
                </LinkButton>

        )
    }

    render() {
        const { searchValue } = this.state

        return (
            <Popup
                searchValue={searchValue}
                onSearchChange={this.onSearchChange}
                onSearchEnter={this.onSearchEnter}
            >
                {this.renderChildren()}
            </Popup>
        )
    }
}

PopupContainer.propTypes = {
    pauseValues: PropTypes.arrayOf(PropTypes.number).isRequired,
}
PopupContainer.defaultProps = {
    pauseValues: [5, 10, 20, 30, 60, 120, 180, Infinity],
}

export default PopupContainer
<|endoftext|>


<commit_before>'use strict';

var stream = require("stream");
var duplexer = require("duplexer2");
var fs = require("fs");
var tmp = require("tempfile");
var cp = require("child_process");
var quotemeta = require("quotemeta");

/**
 * Return first argument if it is the path of a temp file (i.e. a string), and null if it is a stream
 * @param fileOrStream
 */
function tmpFile(fileOrStream) {
    if (typeof fileOrStream === "string") {
        return fileOrStream;
    } else {
        return null;
    }
}

/**
 * Base function to create streams of something, if values are provided for tmpIn (or tmpOut),
 * these are passed to the callback function. If not, the input stream (or the output stream) are used instead.
 *
 * @param tmpIn path to a tempfile to write input stream input to before calling the callback,
 *  if this is null, the stream input is directly
 * @param tmpOut path to a tempfile to which the
 * @param callback function(tmpOrInputStream, tmpOrOutputStream, callback(err)) the callback is called whenever the input
 *   is available.
 * @returns {Stream}
 */
function createStream(tmpIn, tmpOut, callback) {

    var incoming = new stream.PassThrough();
    var outgoing = new stream.PassThrough();
    // input paramter of the child process

    inStream(incoming, tmpIn, function (err) {
        if (err) {
            outgoing.emit("error", err);
        } else {
            callback(tmpIn || incoming, tmpOut || outgoing, function (err) {
                if (err) {
                    outgoing.emit("error", err);
                    if (tmpIn) {
                        fs.unlink(tmpIn);
                    }
                    if (tmpOut) {
                        fs.unlink(tmpOut);
                    }
                    return;
                }
                // tmpFile is not needed anymore
                if (tmpIn) {
                    fs.unlink(tmpIn);
                }
                if (tmpOut) {
                    var out = fs.createReadStream(tmpOut);
                    out.pipe(outgoing);
                    out.on("end", function () {
                        fs.unlink(tmpOut);
                    });
                }
            });
        }
    });
    return duplexer(incoming, outgoing);
}

/**
 *
 * @param stream
 * @param tmpFile path to a temporary file. If this is set, the stream is written to the file
 *    and the file is used as parameter for the callback
 * @param callback
 */
function inStream(stream, tmpFile, callback) {
    if (tmpFile) {
        var tmpWrite = fs.createWriteStream(tmpFile);
        stream.pipe(tmpWrite);
        tmpWrite.on("finish", function () {
            callback(null);
        });
        tmpWrite.on("error", function (error) {
            callback(error);
        });
    } else {
        callback(null);
    }
}

/**
 * Wraps a process provided by a function in a stream such that the stream input is piped to stdin and stdout is piped to the stream output.
 * If tmpIn is provided, no pipe is set up to stdin. Instead, the data is piped into tmpIn which can than be provided to the process
 * as command line argument. The same applies for stdout.
 * @param processProvider
 * @param tmpIn the location of a temporary file that is filled with the input data.
 * @param tmpOut the location of a temporary file that the process writes to and that is used as source to the stream output.
 */
function wrapProcess(tmpIn, tmpOut, processProvider) {
    return createStream(tmpIn, tmpOut, function (input, output, callback) {
        var process = processProvider(tmpFile(input), tmpFile(output));
        if (!tmpIn) {
            input.pipe(process.stdin);
        }
        if (!tmpOut) {
            process.stdout.pipe(output);
        }
        process.on("exit", function () {
            callback(null);
        });
    });
}

/**
 * Create a new ProcessStreams instance optionally providing placeholder for <INPUT> and <OUTPUT>
 * @param [IN] placeholder for input file
 * @param [OUT] placeholder for output file
 */
module.exports = function (IN, OUT) {
    // Expose IN and OUT to the public, but use local variables internally
    this.IN = IN = IN || "<INPUT>";
    this.OUT = OUT = OUT || "<OUTPUT>";
    var placeHolderRegex = new RegExp("(" + quotemeta(IN) + "|" + quotemeta(OUT) + ")", "g");

    /**
     * Replace placeholders in command line arguments (array)
     * @param args a list of command line arguments (string) which may also contain IN once and OUT once.
     * @return {Object}
     * @param tmpIn temporary file for possible input data.
     * @param tmpOut temporary file for possibe output data.
     */
    function parseArgs(args, tmpIn, tmpOut) {
        var resultIn = null;
        var resultOut = null;
        var resultArgs = args ? args.map(function (arg) {
            var parsed = parseString(arg, tmpIn, tmpOut);
            resultIn = resultIn || parsed.in;
            resultOut = resultOut || parsed.out;
            return parsed.string;
        }) : args;
        return {
            in: resultIn,
            out: resultOut,
            args: resultArgs
        };
    }

    /**
     * Replace placeholders in a string
     * @param string
     * @param tmpIn
     * @param tmpOut
     * @returns {{in: *, out: *, string: (XML|string|void|*)}}
     */
    function parseString(string, tmpIn, tmpOut) {
        var resultIn = null;
        var resultOut = null;
        var resultString = string.replace(placeHolderRegex, function (match) {
            switch (match) {
                case IN:
                    return resultIn = resultIn || tmpIn;
                case OUT:
                    return resultOut = resultOut || tmpOut;
                default:
                    throw new Error("Found '" + match + "'. Placeholder regex not consistent: " + JSON.stringify(placeholders));
            }
        });

        return {
            in: resultIn,
            out: resultOut,
            string: resultString
        };
    }

    /**
     * Like child_process.spawn, but returns a through-stream instead of the child process.
     * The property "process" of the stream contains the child process
     * @param command
     * @param args
     * @param [options]
     * @param [callback]
     */
    this.spawn = function (command, args, options, callback) {
        var parsed = parseArgs(args, tmp(), tmp());
        return wrapProcess(parsed.in, parsed.out, function () {
            return cp.spawn(command, parsed.args, options, callback);
        });

    };
    this.exec = function (command, options, callback) {
        var parsed = parseString(command, tmp(), tmp());
        return wrapProcess(parsed.in, parsed.out, function () {
            return cp.exec(parsed.string, options, callback);
        });
    };
    this.execFile = function (command, args, options, callback) {
        var parsed = parseArgs(args, tmp(), tmp());
        return wrapProcess(parsed.in, parsed.out, function () {
            return cp.execFile(command, parsed.args, options, callback);
        });
    };
    /**
     * Creates a stream using a factory function that connects input and output.
     * @param useTmpIn {boolean} whether to use a temp file as input
     * @param useTmpOut {boolean} whether to use a temp file as output
     * @param callback {function(err:Error,input,output,callback)} callback method that connects the input to the output somehow.
     *  Depending on the useTmpIn and useTmpOut parameter, the input and output parameters of the callback are either strings pointing
     *  to files or streams.
     */
    this.factory = function (useTmpIn, useTmpOut, callback) {
        return createStream(useTmpIn && tmp(), useTmpOut && tmp(), callback);
    };

};

<commit_msg>Check, if tmp-files exists, before deleting them (in case of an error)<commit_after>'use strict';

var stream = require("stream");
var duplexer = require("duplexer2");
var fs = require("fs");
var tmp = require("tempfile");
var cp = require("child_process");
var quotemeta = require("quotemeta");

/**
 * Return first argument if it is the path of a temp file (i.e. a string), and null if it is a stream
 * @param fileOrStream
 */
function tmpFile(fileOrStream) {
    if (typeof fileOrStream === "string") {
        return fileOrStream;
    } else {
        return null;
    }
}

/**
 * Base function to create streams of something, if values are provided for tmpIn (or tmpOut),
 * these are passed to the callback function. If not, the input stream (or the output stream) are used instead.
 *
 * @param tmpIn path to a tempfile to write input stream input to before calling the callback,
 *  if this is null, the stream input is directly
 * @param tmpOut path to a tempfile to which the
 * @param callback function(tmpOrInputStream, tmpOrOutputStream, callback(err)) the callback is called whenever the input
 *   is available.
 * @returns {Stream}
 */
function createStream(tmpIn, tmpOut, callback) {

    var incoming = new stream.PassThrough();
    var outgoing = new stream.PassThrough();
    // input paramter of the child process

    inStream(incoming, tmpIn, function (err) {
        if (err) {
            outgoing.emit("error", err);
        } else {
            callback(tmpIn || incoming, tmpOut || outgoing, function (err) {
                if (err) {
                    outgoing.emit("error", err);
                    if (tmpIn) {
                        fs.exists(tmpIn, function(exists) {
                            if (exists) {
                                fs.unlink(tmpIn);
                            }
                        });
                    }
                    if (tmpOut) {
                        fs.exists(tmpOut, function(exists) {
                            if (exists) {
                                fs.unlink(tmpOut);
                            }
                        });
                    }
                    return;
                }
                // tmpFile is not needed anymore
                if (tmpIn) {
                    fs.unlink(tmpIn);
                }
                if (tmpOut) {
                    var out = fs.createReadStream(tmpOut);
                    out.pipe(outgoing);
                    out.on("end", function () {
                        fs.unlink(tmpOut);
                    });
                }
            });
        }
    });
    return duplexer(incoming, outgoing);
}

/**
 *
 * @param stream
 * @param tmpFile path to a temporary file. If this is set, the stream is written to the file
 *    and the file is used as parameter for the callback
 * @param callback
 */
function inStream(stream, tmpFile, callback) {
    if (tmpFile) {
        var tmpWrite = fs.createWriteStream(tmpFile);
        stream.pipe(tmpWrite);
        tmpWrite.on("finish", function () {
            callback(null);
        });
        tmpWrite.on("error", function (error) {
            callback(error);
        });
    } else {
        callback(null);
    }
}

/**
 * Wraps a process provided by a function in a stream such that the stream input is piped to stdin and stdout is piped to the stream output.
 * If tmpIn is provided, no pipe is set up to stdin. Instead, the data is piped into tmpIn which can than be provided to the process
 * as command line argument. The same applies for stdout.
 * @param processProvider
 * @param tmpIn the location of a temporary file that is filled with the input data.
 * @param tmpOut the location of a temporary file that the process writes to and that is used as source to the stream output.
 */
function wrapProcess(tmpIn, tmpOut, processProvider) {
    return createStream(tmpIn, tmpOut, function (input, output, callback) {
        var process = processProvider(tmpFile(input), tmpFile(output));
        if (!tmpIn) {
            input.pipe(process.stdin);
        }
        if (!tmpOut) {
            process.stdout.pipe(output);
        }
        process.on("exit", function () {
            callback(null);
        });
    });
}

/**
 * Create a new ProcessStreams instance optionally providing placeholder for <INPUT> and <OUTPUT>
 * @param [IN] placeholder for input file
 * @param [OUT] placeholder for output file
 */
module.exports = function (IN, OUT) {
    // Expose IN and OUT to the public, but use local variables internally
    this.IN = IN = IN || "<INPUT>";
    this.OUT = OUT = OUT || "<OUTPUT>";
    var placeHolderRegex = new RegExp("(" + quotemeta(IN) + "|" + quotemeta(OUT) + ")", "g");

    /**
     * Replace placeholders in command line arguments (array)
     * @param args a list of command line arguments (string) which may also contain IN once and OUT once.
     * @return {Object}
     * @param tmpIn temporary file for possible input data.
     * @param tmpOut temporary file for possibe output data.
     */
    function parseArgs(args, tmpIn, tmpOut) {
        var resultIn = null;
        var resultOut = null;
        var resultArgs = args ? args.map(function (arg) {
            var parsed = parseString(arg, tmpIn, tmpOut);
            resultIn = resultIn || parsed.in;
            resultOut = resultOut || parsed.out;
            return parsed.string;
        }) : args;
        return {
            in: resultIn,
            out: resultOut,
            args: resultArgs
        };
    }

    /**
     * Replace placeholders in a string
     * @param string
     * @param tmpIn
     * @param tmpOut
     * @returns {{in: *, out: *, string: (XML|string|void|*)}}
     */
    function parseString(string, tmpIn, tmpOut) {
        var resultIn = null;
        var resultOut = null;
        var resultString = string.replace(placeHolderRegex, function (match) {
            switch (match) {
                case IN:
                    return resultIn = resultIn || tmpIn;
                case OUT:
                    return resultOut = resultOut || tmpOut;
                default:
                    throw new Error("Found '" + match + "'. Placeholder regex not consistent: " + JSON.stringify(placeholders));
            }
        });

        return {
            in: resultIn,
            out: resultOut,
            string: resultString
        };
    }

    /**
     * Like child_process.spawn, but returns a through-stream instead of the child process.
     * The property "process" of the stream contains the child process
     * @param command
     * @param args
     * @param [options]
     * @param [callback]
     */
    this.spawn = function (command, args, options, callback) {
        var parsed = parseArgs(args, tmp(".in"), tmp(".out"));
        return wrapProcess(parsed.in, parsed.out, function () {
            return cp.spawn(command, parsed.args, options, callback);
        });

    };
    this.exec = function (command, options, callback) {
        var parsed = parseString(command, tmp(".in"), tmp(".out"));
        return wrapProcess(parsed.in, parsed.out, function () {
            return cp.exec(parsed.string, options, callback);
        });
    };
    this.execFile = function (command, args, options, callback) {
        var parsed = parseArgs(args, tmp(".in"), tmp(".out"));
        return wrapProcess(parsed.in, parsed.out, function () {
            return cp.execFile(command, parsed.args, options, callback);
        });
    };
    /**
     * Creates a stream using a factory function that connects input and output.
     * @param useTmpIn {boolean} whether to use a temp file as input
     * @param useTmpOut {boolean} whether to use a temp file as output
     * @param callback {function(err:Error,input,output,callback)} callback method that connects the input to the output somehow.
     *  Depending on the useTmpIn and useTmpOut parameter, the input and output parameters of the callback are either strings pointing
     *  to files or streams.
     */
    this.factory = function (useTmpIn, useTmpOut, callback) {
        return createStream(useTmpIn && tmp(".in"), useTmpOut && tmp(".out"), callback);
    };

};

<|endoftext|>


<commit_before>import stringSimilarity from 'string-similarity';
import gql from './gql';

function createPostbackAction(label, input, issuedAt) {
  return {
    type: 'postback',
    label,
    data: JSON.stringify({
      input,
      issuedAt,
    }),
  };
}

const SIMILARITY_THRESHOLD = 0.95;

// State diagram:
// http://bit.ly/2kZY6kL
//
export default async function processMessages(
  { state = '__INIT__', data = {} },
  event,
  issuedAt, // When this request is issued. Will be written in postback replies.
  userId
) {
  let replies;

  if (event.input.length >= 3) {
    // If input contains more than 3 words,
    // consider it as a new query and start over.
    data = {};
    state = '__INIT__';
  }

  // Sets state, data and replies
  //
  switch (state) {
    case '__INIT__': {
      // Store user input into context
      data.searchedText = event.input;

      // Search for articles
      const { data: { SearchArticles } } = await gql`query($text: String!) {
        SearchArticles(text: $text, orderBy: [{_score: DESC}], first: 4) {
          edges {
            node {
              text
              id
            }
          }
        }
      }`({
        text: event.input,
      });

      const articleSummary = `${event.input.slice(0, 10)}${event.input.length > 10 ? '⋯⋯' : ''}`;
      if (SearchArticles.edges.length) {
        if (SearchArticles.edges.length === 1) {
          const foundText = SearchArticles.edges[0].node.text;
          const similarity = stringSimilarity.compareTwoStrings(event.input, foundText);
          if (similarity >= SIMILARITY_THRESHOLD) {
            // choose for user
            event.input = 1;

            // Store article ids
            data.foundArticleIds = SearchArticles.edges.map(
              ({ node: { id } }) => id
            );
            return processMessages(
              {
                state: 'CHOOSING_ARTICLE',
                data,
              },
              event,
              issuedAt,
              userId
            );
          }
        }

        const templateMessage = {
          type: 'template',
          altText: SearchArticles.edges
            .map(
              (
                { node: { text } },
                idx
              ) => `選擇請打 ${idx + 1}> ${text.slice(0, 20)}`
            )
            .concat(['若以上皆非，請打 0。'])
            .join('\n\n'),
          template: {
            type: 'carousel',
            columns: SearchArticles.edges.map(({ node: { text } }, idx) => ({
              text: `[相似度:${stringSimilarity.compareTwoStrings(event.input, text)}] \n ${text.slice(0, 105)}`,
              actions: [
                createPostbackAction('選擇此則', idx + 1, issuedAt),
              ],
            })).concat([{
              text: '這裡沒有一篇是我傳的訊息。',
              actions: [
                createPostbackAction('選擇', 0, issuedAt),
              ],
            }]),
          },
        };

        // Store article ids
        data.foundArticleIds = SearchArticles.edges.map(
          ({ node: { id } }) => id
        );

        replies = [
          {
            type: 'text',
            text: `幫您查詢「${articleSummary}」的相關回應。`,
          },
          {
            type: 'text',
            text: '請問下列文章中，哪一篇是您剛才傳送的訊息呢？',
          },
          templateMessage,
        ];
        state = 'CHOOSING_ARTICLE';
      } else {
        replies = [
          {
            type: 'text',
            text: `找不到關於「${articleSummary}」文章耶 QQ`,
          },
          {
            type: 'template',
            altText: '請問要將這份文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
            template: {
              type: 'buttons',
              text: '請問要將這份文章送出到資料庫嗎？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];
        state = 'ASKING_ARTICLE_SUBMISSION';
      }

      break;
    }
    case 'CHOOSING_ARTICLE': {
      if (!data.foundArticleIds) {
        throw new Error('foundArticleIds not set in data');
      }

      const selectedArticleId = data.foundArticleIds[event.input - 1];

      if (+event.input === 0) {
        replies = [
          {
            type: 'template',
            altText: '請問要將文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
            template: {
              type: 'buttons',
              text: '請問要將文章送出到資料庫嗎？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];

        state = 'ASKING_ARTICLE_SUBMISSION';
      } else if (!selectedArticleId) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundArticleIds.length} 的數字。` },
        ];

        state = 'CHOOSING_ARTICLE';
      } else {
        const { data: { GetArticle } } = await gql`query($id: String!) {
          GetArticle(id: $id) {
            replyCount
            replyConnections {
              id
              reply {
                id
                versions(limit: 1) {
                  type
                  text
                }
              }
            }
          }
        }`({
          id: selectedArticleId,
        });

        const {
          rumorReplies,
          notRumorReplies,
        } = GetArticle.replyConnections.reduce(
            (result, { reply, id }) => {
              if (reply.versions[0].type === 'RUMOR') {
                result.rumorReplies.push({ ...reply, replyConnectionId: id });
              } else if (reply.versions[0].type === 'NOT_RUMOR') {
                result.notRumorReplies.push({ ...reply, replyConnectionId: id });
              }
              return result;
            },
            { rumorReplies: [], notRumorReplies: [] }
          );

        replies = [];

        if (notRumorReplies.length + rumorReplies.length !== 0) {
          data.foundReplies = notRumorReplies.concat(rumorReplies).map(({ replyConnectionId, id }) => ({
            id,
            replyConnectionId,
          }));
          state = 'CHOOSING_REPLY';

          if (notRumorReplies.length + rumorReplies.length === 1) {

            // choose for user
            event.input = 1;

            return processMessages(
              {
                state: 'CHOOSING_REPLY',
                data,
              },
              event,
              issuedAt,
              userId
            );
          }

          if (notRumorReplies.length) {
            replies.push({
              type: 'text',
              text: `這篇文章有 ${notRumorReplies.length} 個回應表示查無不實：`,
            });
            replies.push({
              type: 'template',
              altText: notRumorReplies
                .map(
                (
                  { versions },
                  idx
                ) => `閱讀請傳 ${idx + 1}> ${versions[0].text.slice(0, 20)}`
                )
                .join('\n\n'),
              template: {
                type: 'carousel',
                columns: notRumorReplies.map(({ versions }, idx) => ({
                  text: versions[0].text.slice(0, 119),
                  actions: [createPostbackAction('閱讀此回應', idx + 1, issuedAt)],
                })),
              },
            });
          }
          if (rumorReplies.length) {
            replies.push({
              type: 'text',
              text: `這篇文章有 ${rumorReplies.length} 個回應覺得有不實之處：`,
            });
            replies.push({
              type: 'template',
              altText: rumorReplies
                .map(
                (
                  { versions },
                  idx
                ) => `閱讀請傳 ${notRumorReplies.length + idx + 1}> ${versions[0].text.slice(0, 20)}`
                )
                .join('\n\n'),
              template: {
                type: 'carousel',
                columns: rumorReplies.map(({ versions }, idx) => ({
                  text: versions[0].text.slice(0, 119),
                  actions: [
                    createPostbackAction(
                      '閱讀此回應',
                      notRumorReplies.length + idx + 1,
                      issuedAt
                    ),
                  ],
                })),
              },
            });
          }
        } else if (GetArticle.replyCount !== 0) {
          // [replyCount !=0 && replies == 0]
          // Someone reported that it is not an article,
          // and there are no other replies available.
          // FIXME (ggm) very tricky if condition here
          replies = [
            {
              type: 'text',
              text: '有人認為您傳送的這則訊息並不是完整的文章內容，或認為「真的假的」不應該處理這則訊息。',
            },
            {
              type: 'text',
              text: `詳情請見：http://rumors.hacktabl.org/article/${selectedArticleId}`,
            },
          ];
          state = '__INIT__';
        } else {
          // [replyCount ==0 && replies == 0]
          // No one has replied to this yet.
          const {
            data: { CreateReplyRequest },
            errors,
          } = await gql`mutation($id: String!) {
            CreateReplyRequest(articleId: $id) {
              replyRequestCount
            }
          }`({ id: selectedArticleId }, { userId });

          replies = [
            {
              type: 'text',
              text: `目前還沒有人回應這篇文章唷。${errors ? '' : `已經將您的需求記錄下來了，共有 ${CreateReplyRequest.replyRequestCount} 人跟您一樣渴望看到針對這篇文章的回應。`}`,
            },
            {
              type: 'text',
              text: `若有最新回應，會寫在這個地方：http://rumors.hacktabl.org/article/${selectedArticleId}`,
            },
          ];

          state = '__INIT__';
        }
      }

      break;
    }
    case 'CHOOSING_REPLY': {
      if (!data.foundReplies) {
        throw new Error('foundReplies not set in data');
      }

      const selectedReply = data.foundReplies[event.input - 1];

      if (!selectedReply) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundReplies.length} 的數字。` },
        ];

        state = 'CHOOSING_REPLY';
      } else {
        const { data: { GetReply } } = await gql`query($id: String!) {
          GetReply(id: $id) {
            versions(limit: 1) {
              type
              text
              reference
              createdAt
            }
          }
        }`({ id: selectedReply.id });

        replies = [
          {
            type: 'text',
            text: `這則回應認為文章${GetReply.versions[0].type === 'RUMOR' ? '含有不實訊息' : '不含不實訊息'}，理由為：`,
          },
          { type: 'text', text: GetReply.versions[0].text },
          { type: 'text', text: `出處：${GetReply.versions[0].reference}` },
          {
            type: 'template',
            altText: '請問這則回應是否有解答原文章？\n「是」請輸入「y」，「否」請輸入其他任何訊息。',
            template: {
              type: 'buttons',
              text: '請問這則回應是否有解答原文章？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];

        data.selectedReply = selectedReply;
        state = 'ASKING_REPLY_FEEDBACK';
      }

      break;
    }
    case 'ASKING_REPLY_FEEDBACK': {
      if (!data.selectedReply) {
        throw new Error('selectedReply not set in data');
      }

      const {
        data: { action: { feedbackCount } },
      } = await gql`mutation($vote: FeedbackVote!, $id: String!){
        action: CreateOrUpdateReplyConnectionFeedback(
          vote: $vote
          replyConnectionId: $id
        ) {
          feedbackCount
        }
      }`(
        {
          id: data.selectedReply.replyConnectionId,
          vote: event.input === 'y' ? 'UPVOTE' : 'DOWNVOTE',
        },
        { userId }
        );

      replies = [
        {
          type: 'text',
          text: feedbackCount > 1
            ? `感謝您與其他 ${feedbackCount - 1} 人的回饋。`
            : '感謝您的回饋，您是第一個評論這份文章與回應的人 :)',
        },
      ];

      state = '__INIT__';
      break;
    }
    case 'ASKING_ARTICLE_SUBMISSION': {
      if (!data.searchedText) {
        throw new Error('searchText not set in data');
      }

      if (event.input === 'y') {
        const { data: { CreateArticle } } = await gql`mutation($text: String!){
          CreateArticle(text: $text, reference: {type: LINE}) {
            id
          }
        }`({ text: data.searchedText }, { userId });

        replies = [
          {
            type: 'text',
            text: `您回報的文章已經被收錄至：http://rumors.hacktabl.org/article/${CreateArticle.id}`,
          },
          { type: 'text', text: '感謝您的回報！' },
        ];
      } else {
        replies = [{ type: 'text', text: '感謝您的使用。' }];
      }
      state = '__INIT__';

      break;
    }

    default:
      replies = [
        {
          type: 'text',
          text: '我們看不懂 QQ\n大俠請重新來過。',
        },
      ];
      state = '__INIT__';
  }

  return {
    context: { state, data },
    replies,
  };
}
<commit_msg>Use fixed-point notation to avoid too large message<commit_after>import stringSimilarity from 'string-similarity';
import gql from './gql';

function createPostbackAction(label, input, issuedAt) {
  return {
    type: 'postback',
    label,
    data: JSON.stringify({
      input,
      issuedAt,
    }),
  };
}

const SIMILARITY_THRESHOLD = 0.95;

// State diagram:
// http://bit.ly/2kZY6kL
//
export default async function processMessages(
  { state = '__INIT__', data = {} },
  event,
  issuedAt, // When this request is issued. Will be written in postback replies.
  userId
) {
  let replies;

  if (event.input.length >= 3) {
    // If input contains more than 3 words,
    // consider it as a new query and start over.
    data = {};
    state = '__INIT__';
  }

  // Sets state, data and replies
  //
  switch (state) {
    case '__INIT__': {
      // Store user input into context
      data.searchedText = event.input;

      // Search for articles
      const { data: { SearchArticles } } = await gql`query($text: String!) {
        SearchArticles(text: $text, orderBy: [{_score: DESC}], first: 4) {
          edges {
            node {
              text
              id
            }
          }
        }
      }`({
        text: event.input,
      });

      const articleSummary = `${event.input.slice(0, 10)}${event.input.length > 10 ? '⋯⋯' : ''}`;
      if (SearchArticles.edges.length) {
        if (SearchArticles.edges.length === 1) {
          const foundText = SearchArticles.edges[0].node.text;
          const similarity = stringSimilarity.compareTwoStrings(event.input, foundText);
          if (similarity >= SIMILARITY_THRESHOLD) {
            // choose for user
            event.input = 1;

            // Store article ids
            data.foundArticleIds = SearchArticles.edges.map(
              ({ node: { id } }) => id
            );
            return processMessages(
              {
                state: 'CHOOSING_ARTICLE',
                data,
              },
              event,
              issuedAt,
              userId
            );
          }
        }

        const templateMessage = {
          type: 'template',
          altText: SearchArticles.edges
            .map(
              (
                { node: { text } },
                idx
              ) => `選擇請打 ${idx + 1}> ${text.slice(0, 20)}`
            )
            .concat(['若以上皆非，請打 0。'])
            .join('\n\n'),
          template: {
            type: 'carousel',
            columns: SearchArticles.edges.map(({ node: { text } }, idx) => ({
              text: `[相似度:${stringSimilarity.compareTwoStrings(event.input, text).toFixed(2)}] \n ${text.slice(0, 105)}`,
              actions: [
                createPostbackAction('選擇此則', idx + 1, issuedAt),
              ],
            })).concat([{
              text: '這裡沒有一篇是我傳的訊息。',
              actions: [
                createPostbackAction('選擇', 0, issuedAt),
              ],
            }]),
          },
        };

        // Store article ids
        data.foundArticleIds = SearchArticles.edges.map(
          ({ node: { id } }) => id
        );

        replies = [
          {
            type: 'text',
            text: `幫您查詢「${articleSummary}」的相關回應。`,
          },
          {
            type: 'text',
            text: '請問下列文章中，哪一篇是您剛才傳送的訊息呢？',
          },
          templateMessage,
        ];
        state = 'CHOOSING_ARTICLE';
      } else {
        replies = [
          {
            type: 'text',
            text: `找不到關於「${articleSummary}」文章耶 QQ`,
          },
          {
            type: 'template',
            altText: '請問要將這份文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
            template: {
              type: 'buttons',
              text: '請問要將這份文章送出到資料庫嗎？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];
        state = 'ASKING_ARTICLE_SUBMISSION';
      }

      break;
    }
    case 'CHOOSING_ARTICLE': {
      if (!data.foundArticleIds) {
        throw new Error('foundArticleIds not set in data');
      }

      const selectedArticleId = data.foundArticleIds[event.input - 1];

      if (+event.input === 0) {
        replies = [
          {
            type: 'template',
            altText: '請問要將文章送出到資料庫嗎？\n「是」請輸入「y」，「否」請輸入「n」或其他單一字母。',
            template: {
              type: 'buttons',
              text: '請問要將文章送出到資料庫嗎？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];

        state = 'ASKING_ARTICLE_SUBMISSION';
      } else if (!selectedArticleId) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundArticleIds.length} 的數字。` },
        ];

        state = 'CHOOSING_ARTICLE';
      } else {
        const { data: { GetArticle } } = await gql`query($id: String!) {
          GetArticle(id: $id) {
            replyCount
            replyConnections {
              id
              reply {
                id
                versions(limit: 1) {
                  type
                  text
                }
              }
            }
          }
        }`({
          id: selectedArticleId,
        });

        const {
          rumorReplies,
          notRumorReplies,
        } = GetArticle.replyConnections.reduce(
            (result, { reply, id }) => {
              if (reply.versions[0].type === 'RUMOR') {
                result.rumorReplies.push({ ...reply, replyConnectionId: id });
              } else if (reply.versions[0].type === 'NOT_RUMOR') {
                result.notRumorReplies.push({ ...reply, replyConnectionId: id });
              }
              return result;
            },
            { rumorReplies: [], notRumorReplies: [] }
          );

        replies = [];

        if (notRumorReplies.length + rumorReplies.length !== 0) {
          data.foundReplies = notRumorReplies.concat(rumorReplies).map(({ replyConnectionId, id }) => ({
            id,
            replyConnectionId,
          }));
          state = 'CHOOSING_REPLY';

          if (notRumorReplies.length + rumorReplies.length === 1) {

            // choose for user
            event.input = 1;

            return processMessages(
              {
                state: 'CHOOSING_REPLY',
                data,
              },
              event,
              issuedAt,
              userId
            );
          }

          if (notRumorReplies.length) {
            replies.push({
              type: 'text',
              text: `這篇文章有 ${notRumorReplies.length} 個回應表示查無不實：`,
            });
            replies.push({
              type: 'template',
              altText: notRumorReplies
                .map(
                (
                  { versions },
                  idx
                ) => `閱讀請傳 ${idx + 1}> ${versions[0].text.slice(0, 20)}`
                )
                .join('\n\n'),
              template: {
                type: 'carousel',
                columns: notRumorReplies.map(({ versions }, idx) => ({
                  text: versions[0].text.slice(0, 119),
                  actions: [createPostbackAction('閱讀此回應', idx + 1, issuedAt)],
                })),
              },
            });
          }
          if (rumorReplies.length) {
            replies.push({
              type: 'text',
              text: `這篇文章有 ${rumorReplies.length} 個回應覺得有不實之處：`,
            });
            replies.push({
              type: 'template',
              altText: rumorReplies
                .map(
                (
                  { versions },
                  idx
                ) => `閱讀請傳 ${notRumorReplies.length + idx + 1}> ${versions[0].text.slice(0, 20)}`
                )
                .join('\n\n'),
              template: {
                type: 'carousel',
                columns: rumorReplies.map(({ versions }, idx) => ({
                  text: versions[0].text.slice(0, 119),
                  actions: [
                    createPostbackAction(
                      '閱讀此回應',
                      notRumorReplies.length + idx + 1,
                      issuedAt
                    ),
                  ],
                })),
              },
            });
          }
        } else if (GetArticle.replyCount !== 0) {
          // [replyCount !=0 && replies == 0]
          // Someone reported that it is not an article,
          // and there are no other replies available.
          // FIXME (ggm) very tricky if condition here
          replies = [
            {
              type: 'text',
              text: '有人認為您傳送的這則訊息並不是完整的文章內容，或認為「真的假的」不應該處理這則訊息。',
            },
            {
              type: 'text',
              text: `詳情請見：http://rumors.hacktabl.org/article/${selectedArticleId}`,
            },
          ];
          state = '__INIT__';
        } else {
          // [replyCount ==0 && replies == 0]
          // No one has replied to this yet.
          const {
            data: { CreateReplyRequest },
            errors,
          } = await gql`mutation($id: String!) {
            CreateReplyRequest(articleId: $id) {
              replyRequestCount
            }
          }`({ id: selectedArticleId }, { userId });

          replies = [
            {
              type: 'text',
              text: `目前還沒有人回應這篇文章唷。${errors ? '' : `已經將您的需求記錄下來了，共有 ${CreateReplyRequest.replyRequestCount} 人跟您一樣渴望看到針對這篇文章的回應。`}`,
            },
            {
              type: 'text',
              text: `若有最新回應，會寫在這個地方：http://rumors.hacktabl.org/article/${selectedArticleId}`,
            },
          ];

          state = '__INIT__';
        }
      }

      break;
    }
    case 'CHOOSING_REPLY': {
      if (!data.foundReplies) {
        throw new Error('foundReplies not set in data');
      }

      const selectedReply = data.foundReplies[event.input - 1];

      if (!selectedReply) {
        replies = [
          { type: 'text', text: `請輸入 1～${data.foundReplies.length} 的數字。` },
        ];

        state = 'CHOOSING_REPLY';
      } else {
        const { data: { GetReply } } = await gql`query($id: String!) {
          GetReply(id: $id) {
            versions(limit: 1) {
              type
              text
              reference
              createdAt
            }
          }
        }`({ id: selectedReply.id });

        replies = [
          {
            type: 'text',
            text: `這則回應認為文章${GetReply.versions[0].type === 'RUMOR' ? '含有不實訊息' : '不含不實訊息'}，理由為：`,
          },
          { type: 'text', text: GetReply.versions[0].text },
          { type: 'text', text: `出處：${GetReply.versions[0].reference}` },
          {
            type: 'template',
            altText: '請問這則回應是否有解答原文章？\n「是」請輸入「y」，「否」請輸入其他任何訊息。',
            template: {
              type: 'buttons',
              text: '請問這則回應是否有解答原文章？',
              actions: [
                createPostbackAction('是', 'y', issuedAt),
                createPostbackAction('否', 'n', issuedAt),
              ],
            },
          },
        ];

        data.selectedReply = selectedReply;
        state = 'ASKING_REPLY_FEEDBACK';
      }

      break;
    }
    case 'ASKING_REPLY_FEEDBACK': {
      if (!data.selectedReply) {
        throw new Error('selectedReply not set in data');
      }

      const {
        data: { action: { feedbackCount } },
      } = await gql`mutation($vote: FeedbackVote!, $id: String!){
        action: CreateOrUpdateReplyConnectionFeedback(
          vote: $vote
          replyConnectionId: $id
        ) {
          feedbackCount
        }
      }`(
        {
          id: data.selectedReply.replyConnectionId,
          vote: event.input === 'y' ? 'UPVOTE' : 'DOWNVOTE',
        },
        { userId }
        );

      replies = [
        {
          type: 'text',
          text: feedbackCount > 1
            ? `感謝您與其他 ${feedbackCount - 1} 人的回饋。`
            : '感謝您的回饋，您是第一個評論這份文章與回應的人 :)',
        },
      ];

      state = '__INIT__';
      break;
    }
    case 'ASKING_ARTICLE_SUBMISSION': {
      if (!data.searchedText) {
        throw new Error('searchText not set in data');
      }

      if (event.input === 'y') {
        const { data: { CreateArticle } } = await gql`mutation($text: String!){
          CreateArticle(text: $text, reference: {type: LINE}) {
            id
          }
        }`({ text: data.searchedText }, { userId });

        replies = [
          {
            type: 'text',
            text: `您回報的文章已經被收錄至：http://rumors.hacktabl.org/article/${CreateArticle.id}`,
          },
          { type: 'text', text: '感謝您的回報！' },
        ];
      } else {
        replies = [{ type: 'text', text: '感謝您的使用。' }];
      }
      state = '__INIT__';

      break;
    }

    default:
      replies = [
        {
          type: 'text',
          text: '我們看不懂 QQ\n大俠請重新來過。',
        },
      ];
      state = '__INIT__';
  }

  return {
    context: { state, data },
    replies,
  };
}
<|endoftext|>


<commit_before>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * All rights reserved.
 */

var Project = this.Project = Base.extend({
	beans: true,

	// TODO: Add arguments to define pages
	initialize: function() {
		// Store reference to the currently active global paper scope:
		this._scope = paper;
		// Push it onto this._scope.projects and set index:
		this._index = this._scope.projects.push(this) - 1;
		this._currentStyle = PathStyle.create(null);
		this._selectedItems = {};
		this._selectedItemCount = 0;
		// Activate straight away so paper.project is set, as required by
		// Layer and DoumentView constructors.
		this.activate();
		this.layers = [];
		this.symbols = [];
		this.activeLayer = new Layer();
	},

	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		this._currentStyle.initialize(style);
	},

	activate: function() {
		if (this._index != null) {
			this._scope.project = this;
			return true;
		}
		return false;
	},

	remove: function() {
		var res = Base.splice(this._scope.projects, null, this._index, 1);
		this._scope = null;
		return !!res.length;
	},

	getIndex: function() {
		return this._index;
	},

	getSelectedItems: function() {
		// TODO: return groups if their children are all selected,
		// and filter out their children from the list.
		// TODO: the order of these items should be that of their
		// drawing order.
		var items = [];
		Base.each(this._selectedItems, function(item) {
			items.push(item);
		});
		return items;
	},

	// TODO: implement setSelectedItems?

	_selectItem: function(item, select) {
		if (select) {
			this._selectedItemCount++;
			this._selectedItems[item.getId()] = item;
		} else {
			this._selectedItemCount--;
			delete this._selectedItems[item.getId()];
		}
	},

	/**
	 * Selects all items in the project.
	 */
	selectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i = 0, l = this.layers.length; i < l; i++)
			this.layers[i].setSelected(true);
	},

	/**
	 * Deselects all selected items in the project.
	 */
	deselectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i in this._selectedItems)
			this._selectedItems[i].setSelected(false);
	},

	draw: function(ctx) {
		ctx.save();
		var param = { offset: new Point(0, 0) };
		for (var i = 0, l = this.layers.length; i < l; i++)
			Item.draw(this.layers[i], ctx, param);
		ctx.restore();

		// Draw the selection of the selected items in the project:
		if (this._selectedItemCount > 0) {
			ctx.save();
			ctx.strokeWidth = 1;
			// TODO: use Layer#color
			ctx.strokeStyle = ctx.fillStyle = '#009dec';
			param = { selection: true };
			Base.each(this._selectedItems, function(item) {
				item.draw(ctx, param);
			});
			ctx.restore();
		}
	},

	/**
	 * @deprecated
	 */
	redraw: function() {
		this._scope.view.draw();
	}
});
<commit_msg>Project#selectAll: remove todo.<commit_after>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * All rights reserved.
 */

var Project = this.Project = Base.extend({
	beans: true,

	// TODO: Add arguments to define pages
	initialize: function() {
		// Store reference to the currently active global paper scope:
		this._scope = paper;
		// Push it onto this._scope.projects and set index:
		this._index = this._scope.projects.push(this) - 1;
		this._currentStyle = PathStyle.create(null);
		this._selectedItems = {};
		this._selectedItemCount = 0;
		// Activate straight away so paper.project is set, as required by
		// Layer and DoumentView constructors.
		this.activate();
		this.layers = [];
		this.symbols = [];
		this.activeLayer = new Layer();
	},

	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		this._currentStyle.initialize(style);
	},

	activate: function() {
		if (this._index != null) {
			this._scope.project = this;
			return true;
		}
		return false;
	},

	remove: function() {
		var res = Base.splice(this._scope.projects, null, this._index, 1);
		this._scope = null;
		return !!res.length;
	},

	getIndex: function() {
		return this._index;
	},

	getSelectedItems: function() {
		// TODO: return groups if their children are all selected,
		// and filter out their children from the list.
		// TODO: the order of these items should be that of their
		// drawing order.
		var items = [];
		Base.each(this._selectedItems, function(item) {
			items.push(item);
		});
		return items;
	},

	// TODO: implement setSelectedItems?

	_selectItem: function(item, select) {
		if (select) {
			this._selectedItemCount++;
			this._selectedItems[item.getId()] = item;
		} else {
			this._selectedItemCount--;
			delete this._selectedItems[item.getId()];
		}
	},

	/**
	 * Selects all items in the project.
	 */
	selectAll: function() {
		for (var i = 0, l = this.layers.length; i < l; i++)
			this.layers[i].setSelected(true);
	},

	/**
	 * Deselects all selected items in the project.
	 */
	deselectAll: function() {
		// TODO: is using for var i in good practice?
		// or should we use Base.each? (JP)
		for (var i in this._selectedItems)
			this._selectedItems[i].setSelected(false);
	},

	draw: function(ctx) {
		ctx.save();
		var param = { offset: new Point(0, 0) };
		for (var i = 0, l = this.layers.length; i < l; i++)
			Item.draw(this.layers[i], ctx, param);
		ctx.restore();

		// Draw the selection of the selected items in the project:
		if (this._selectedItemCount > 0) {
			ctx.save();
			ctx.strokeWidth = 1;
			// TODO: use Layer#color
			ctx.strokeStyle = ctx.fillStyle = '#009dec';
			param = { selection: true };
			Base.each(this._selectedItems, function(item) {
				item.draw(ctx, param);
			});
			ctx.restore();
		}
	},

	/**
	 * @deprecated
	 */
	redraw: function() {
		this._scope.view.draw();
	}
});
<|endoftext|>