Last active
February 18, 2016 04:34
-
-
Save pstephenson02/05a6dd1d35fa40999a44 to your computer and use it in GitHub Desktop.
Phil Stephenson's Code Samples!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import HipchatterBase from 'hipchatter'; | |
import trebek from './trebek'; | |
import winston from 'winston'; | |
import fetch from 'node-fetch'; | |
import moment from 'moment'; | |
import App from './models/App'; | |
import { HIPCHAT_CALLBACK_URL } from './config'; | |
export default class Hipchatter extends HipchatterBase { | |
get_session(callback) { | |
this.request('get', 'oauth/token/'+this.token, callback); | |
} | |
} | |
export default class HipchatBot { | |
constructor(express) { | |
this.express = express; | |
this.buildCapabilitiesDescriptor = this.buildCapabilitiesDescriptor.bind(this); | |
this.install = this.install.bind(this); | |
this.uninstall = this.uninstall.bind(this); | |
this.onMessage = this.onMessage.bind(this); | |
this.onRoomEnter = this.onRoomEnter.bind(this); | |
this.onError = this.onError.bind(this); | |
this.say = this.say.bind(this); | |
this.start(); | |
} | |
async start() { | |
this.express.get('/capabilities', this.buildCapabilitiesDescriptor); | |
this.express.post('/install', this.install); | |
this.express.delete('/install/*', this.uninstall); | |
this.express.post('/room_message', this.onMessage); | |
this.express.post('/room_enter', this.onRoomEnter); | |
this.app = await App.get(); | |
if (!this.app.hipchat.oauthId || !this.app.hipchat.oauthSecret) { | |
throw new Error('Before you can run Jeopardy Bot on Hipchat, you must first install it as an add-on.'); | |
} | |
await this.validateToken(this.app.hipchat); | |
this.hipchatter = new Hipchatter(this.app.hipchat.accessToken.token); | |
const roomId = await this.getInstalledRoomId(); | |
this.registerWebhook(roomId, 'room_message'); | |
this.registerWebhook(roomId, 'room_enter'); | |
} | |
getInstalledRoomId() { | |
return new Promise(resolve => { | |
this.hipchatter.get_session((err, session) => { | |
resolve(session.client.room.id); | |
}); | |
}); | |
} | |
async validateToken(hipchat) { | |
if (!hipchat.accessToken.token || moment().isAfter(hipchat.accessToken.expires_in)) { | |
const { access_token: token, expires_in: expires } = await this.getAccessToken(hipchat.oauthId, hipchat.oauthSecret); | |
this.app.hipchat.accessToken.token = token; | |
this.app.hipchat.accessToken.expires = moment().add(expires, 'seconds'); | |
await this.app.save(); | |
} | |
} | |
buildCapabilitiesDescriptor(req, res) { | |
const descriptor = { | |
"name": "Jeopardy Bot", | |
"description": "This is Jeopardy!", | |
"key": "com.jeopardy.bot", | |
"links": { | |
"homepage": "https://github.com/kesne/jeopardy-bot", | |
"self": HIPCHAT_CALLBACK_URL + "/capabilities", | |
}, | |
"capabilities": { | |
"hipchatApiConsumer": { | |
"scopes": [ | |
"admin_room", | |
"send_notification", | |
] | |
}, | |
"installable": { | |
"callbackUrl": HIPCHAT_CALLBACK_URL + "/install", | |
"allowGlobal": false, | |
"allowRoom": true, | |
}, | |
}, | |
} | |
res.send(descriptor); | |
} | |
async install(req, res) { | |
const { capabilitiesUrl, oauthId, roomId, oauthSecret } = req.body; | |
const tokenUrl = await this.getTokenUrl(capabilitiesUrl); | |
const { access_token: token, expires_in: expires } = await this.getAccessToken(oauthId, oauthSecret, tokenUrl); | |
this.app.hipchat.oauthId = oauthId; | |
this.app.hipchat.oauthSecret = oauthSecret; | |
this.app.hipchat.accessToken.token = token; | |
this.app.hipchat.accessToken.expires = moment().add(expires, 'seconds'); | |
await this.app.save(); | |
winston.info(`JeopardyBot successfully installed in room ${roomId}.`); | |
res.sendStatus(200); | |
this.start(); | |
} | |
uninstall(req, res) { | |
// Hipchat will cleanup any webhooks created with this token, so no need to manually do that | |
this.app.collection.updateOne({ _id: this.app._id }, { | |
$unset: { | |
'hipchat.oauthId': this.app.hipchat.oauthId, | |
'hipchat.oauthSecret': this.app.hipchat.oauthSecret, | |
'hipchat.accessToken.token': this.app.hipchat.accessToken.token, | |
}, | |
$pullAll: { | |
webhooks: this.app.hipchat.webhooks, | |
}, | |
}, async (err, result) => { | |
if (err === null) { | |
await this.app.save(); | |
res.sendStatus(200); | |
} else { | |
this.onError(err); | |
} | |
}); | |
} | |
getTokenUrl(tokenUrl) { | |
return fetch(tokenUrl) | |
.then(res => res.json()) | |
.then(json => json.capabilities.oauth2Provider.tokenUrl); | |
} | |
getAccessToken(oauthId, oauthSecret, tokenUrl = 'https://api.hipchat.com/v2/oauth/token') { | |
const auth = new Buffer(`${oauthId}:${oauthSecret}`).toString('base64'); | |
return fetch(tokenUrl, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/x-www-form-urlencoded', | |
'Authorization': 'Basic '+auth, | |
}, | |
body: 'grant_type=client_credentials&scope[]=admin_room&scope[]=send_notification' | |
}).then(res => res.json()); | |
} | |
onMessage(req) { | |
const { | |
event: subtype, | |
item: { | |
room: { name: channel_name, }, | |
message: { | |
message: text, | |
from: { | |
id: user_id, | |
mention_name: user_name, | |
}, | |
}, | |
}, | |
} = req.body; | |
this.channelName = req.body.item.room.name; | |
const channel_id = (req.body.item.room.id).toString(); | |
const timestamp = Date.parse(req.body.item.message.date) / 1000; | |
trebek(text, { | |
subtype, | |
channel_name, | |
channel_id, | |
timestamp, | |
user_id, | |
user_name, | |
}, this.say); | |
} | |
onRoomEnter(req) { | |
const { | |
event: subtype, | |
item: { | |
room: { name: channel_name, }, | |
sender: { | |
id: user_id, | |
mention_name: user_name, | |
}, | |
}, | |
} = req.body; | |
this.channelName = req.body.item.room.name; | |
const channel_id = (req.body.item.room.id).toString(); | |
// The room_enter payload does not include a timestamp | |
const timestamp = moment(); | |
// Just to be consistent with the slack api | |
// https://api.slack.com/events/message/channel_join | |
const text = `@${user_name} has joined the channel`; | |
trebek(text, { | |
subtype, | |
channel_name, | |
channel_id, | |
timestamp, | |
user_id, | |
user_name, | |
}, this.say); | |
} | |
say(message, url = '') { | |
if (url) { | |
message += `<br /><img src="${url}" width="420" height="259" /><br />`; | |
} | |
this.validateToken(this.app.hipchat); | |
this.hipchatter.notify(this.channelName, { message: message, token: this.app.hipchat.accessToken.token }, this.onError); | |
} | |
async registerWebhook(roomId, event) { | |
for (let webhookId of this.app.hipchat.webhooks) { | |
let wh; | |
try { | |
wh = await this.validateWebhook(roomId, webhookId, event); | |
} catch (err) { | |
this.onError(err); | |
} | |
if (wh) { | |
return; | |
} | |
} | |
// If we get here we don't yet have a valid webhook, so let's create one | |
winston.error('No valid webhooks found. Creating a new one...'); | |
this.hipchatter.create_webhook(roomId, { | |
url: `${HIPCHAT_CALLBACK_URL}/${event}`, | |
event: event, | |
}, (err, webhook) => { | |
if (err === null) { | |
this.app.hipchat.webhooks.push(webhook.id); | |
winston.info(`Successfully created ${event} webhook with id: ${webhook.id}.`); | |
this.app.save(); | |
} else { | |
this.onError(err); | |
} | |
}); | |
} | |
validateWebhook(roomId, webhookId, event) { | |
return new Promise((resolve, reject) => { | |
this.hipchatter.get_webhook(roomId, webhookId, (err, webhook) => { | |
if (err === null && webhook.event === event && webhook.url === `${HIPCHAT_CALLBACK_URL}/${event}`) { | |
winston.info(`Existing ${event} webhook ${webhook.id} found and valid.`); | |
resolve(webhook); | |
} else { | |
reject(`${webhookId} is not a valid ${event} webhook for room ${roomId}`); | |
} | |
}); | |
}); | |
} | |
onError(err) { | |
if (err) winston.error(err); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace ICC\VoteBundle\Controller; | |
use FOS\RestBundle\Controller\Annotations\RouteResource; | |
use FOS\RestBundle\Controller\Annotations\QueryParam; | |
use FOS\RestBundle\Controller\Annotations\View; | |
use FOS\RestBundle\Util\Codes; | |
use FOS\RestBundle\View\View as FOSView; | |
use ICC\UtilityBundle\Controller\AbstractController; | |
use ICC\VoteBundle\Entity\ManualBallotOption; | |
use Symfony\Component\HttpFoundation\Request; | |
use Nelmio\ApiDocBundle\Annotation\ApiDoc; | |
use Symfony\Component\HttpKernel\Exception\HttpException; | |
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | |
use ICC\VoteBundle\Model\ManualVoteInterface; | |
use FOS\RestBundle\Request\ParamFetcherInterface; | |
use ICC\AvectraBundle\Entity\User; | |
use FOS\RestBundle\Controller\Annotations\Get; | |
/** | |
* Class ManualPCHVoteController | |
* @RouteResource("ManualPCHVote") | |
*/ | |
class ManualPCHVoteController extends AbstractController | |
{ | |
/** | |
* Get a ManualPCHVote entity | |
* | |
* @param int $id | |
* @View(serializerEnableMaxDepthChecks=true) | |
* | |
* @ApiDoc( | |
* resource = true, | |
* description = "Gets a ManualPCHVote for a given id", | |
* output = "ICC\VoteBundle\Entity\ManualPCHVote", | |
* statusCodes = { | |
* 200 = "Returned when successful", | |
* 404 = "Returned when the vote is not found" | |
* } | |
* ) | |
* | |
* @return ManualVoteInterface | |
* | |
* @throws NotFoundHttpException | |
*/ | |
public function getAction($id) | |
{ | |
if (!($vote = $this->container->get('vote.manual_vote_manager')->get($id))) { | |
throw new NotFoundHttpException(sprintf('The resource \'%s\' was not found.', $id)); | |
} | |
return $vote; | |
} | |
/** | |
* @param int $mbo_id | |
* | |
* @Get("/manualpchvote/{mbo_id}") | |
* | |
* @return mixed | |
*/ | |
public function getVoteByMboAction($mbo_id) | |
{ | |
$user_id = $this->container->get('security.context')->getToken()->getUser()->getId(); | |
return $this->container->get('vote.manual_vote_manager')->getVoteByManualBallotOption($mbo_id, $user_id); | |
} | |
/** | |
* List all votes. | |
* | |
* @ApiDoc( | |
* resource = true, | |
* statusCodes = { | |
* 200 = "Returned when successful" | |
* } | |
* ) | |
* | |
* @QueryParam(name="offset", requirements="\d+", nullable=true, description="Offset from which to start listing votes.") | |
* @QueryParam(name="limit", requirements="\d+", default="5", description="How many votes to return.") | |
* | |
* @param ParamFetcherInterface $paramFetcher param fetcher service | |
* | |
* @return array | |
*/ | |
public function getVotesAction(ParamFetcherInterface $paramFetcher) | |
{ | |
$offset = $paramFetcher->get('offset'); | |
$offset = null == $offset ? 0 : $offset; | |
$limit = $paramFetcher->get('limit'); | |
return $this->container->get('vote.manual_vote_manager')->all($limit, $offset); | |
} | |
/** | |
* Create ManualPCHVote(s). | |
* | |
* @View(statusCode=201, serializerEnableMaxDepthChecks=true) | |
* | |
* @ApiDoc( | |
* resource = true, | |
* description = "Creates new ManualPCHVote(s) given ManualBallotOption id, User id, and vote (ManualPCHVote::SUPPORT/OPPOSE).", | |
* requirements = { | |
* {"name"="user", "dataType"="integer", "required"=true, "description"="Avectra User id"}, | |
* {"name"="manualBallotOption", "dataType"="integer", "required"=true, "description"="ManualBallotOption id"}, | |
* {"name"="vote", "dataType"="integer", "required"=true, "description"="1 (support) or 2 (oppose)"} | |
* }, | |
* statusCodes = { | |
* 200 = "Returned when successful", | |
* 400 = "Returned when the form has errors" | |
* } | |
* ) | |
* | |
* @param Request $request | |
* | |
* @return View | |
*/ | |
public function postAction(Request $request) | |
{ | |
$votePermissions = $this->container->get('vote.permissions'); | |
$user = $votePermissions->getUser(); | |
$votes = $errorData = array(); | |
if (!$votePermissions->canUserVote()) { | |
return $this->createJsonResponse(false, 'vote.permission.not_allowed', Codes::HTTP_FORBIDDEN); | |
} | |
if (is_array($request->request->all())) { | |
foreach ($request->request->all() as $vote) { | |
$pin = isset($vote['user']) ? $vote['user'] : false; | |
if (!$votePermissions->canSubmitManualVote($pin, $errorData)) { | |
// $vote['user'] actually contains the pin | |
return $this->createJsonResponse( | |
false, | |
$errorData['message'], | |
200, | |
isset($errorData['data']) ? $errorData['data'] : array() | |
); | |
} | |
$vote['user'] = $user->getId(); | |
$votes[] = $this->container->get('vote.manual_vote_manager')->post($vote); | |
} | |
} else { | |
throw new HttpException(400); | |
} | |
$response = array( | |
'message_class' => 'ajax-success', | |
'data' => array( | |
'pin_invalid' => false, | |
) | |
); | |
return $this->createJmsResponse($response, true, 'vote.success'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace ICC\VoteBundle\Tests\Controller; | |
use ICC\AvectraBundle\Service\MockUsers; | |
use ICC\SettingBundle\Entity\Setting; | |
use ICC\TestBundle\Lib\Test\WebDoctrineTestCase; | |
use ICC\VoteBundle\Entity\ManualPCHVote; | |
class ManualPCHVoteControllerTest extends WebDoctrineTestCase | |
{ | |
protected $avectraUserProvider; | |
public function setUp() | |
{ | |
parent::setUp(); | |
static::$client->followRedirects(); | |
$this->getContainer()->get('setting.manager')->set(Setting::VOTING_ENABLED, true); | |
$this->avectraUserProvider = $this->getContainer()->get('avectra_user_provider'); | |
} | |
public function testJsonGetManualPCHVoteAction() | |
{ | |
$votes = static::$entityManager->getRepository('VoteBundle:ManualPCHVote')->findAll(); | |
$manualPCHVote = $votes[rand(0, count($votes)-1)]; // Get a random vote | |
$route = $this->getUrl('api_get_manualpchvote', array('id' => $manualPCHVote->getId(), '_format' => 'json')); | |
static::$client->request('GET', $route, array('ACCEPT' => 'application/json')); | |
$response = static::$client->getResponse(); | |
$this->assertJsonResponse($response, 200); | |
$content = $response->getContent(); | |
$decoded = json_decode($content, true); | |
$this->assertTrue(isset($decoded['id'])); | |
} | |
public function testJsonPostManualPCHVoteAction() | |
{ | |
$this->logIn(MockUsers::PCH_USER); | |
$voter = static::$entityManager->getRepository('AvectraBundle:User')->findOneBy(array('firstName' => 'Long Beach Clicker')); | |
$manualBallotOptions = static::$entityManager->getRepository('VoteBundle:ManualBallotOption')->findAll(); | |
$manualBallotOption = $manualBallotOptions[rand(0, count($manualBallotOptions)-1)]; | |
$json_data = array( | |
'manualBallotOption' => $manualBallotOption->getId(), | |
'user' => $this->avectraUserProvider->getPin($voter), | |
'vote' => ManualPCHVote::SUPPORT | |
); | |
static::$client->request( | |
'POST', | |
'/api/manualpchvotes.json', | |
array(), | |
array(), | |
array('CONTENT_TYPE' => 'application/json'), | |
json_encode(array($json_data)) | |
); | |
$this->assertJsonResponse(static::$client->getResponse(), 200, false); | |
} | |
public function testJsonBulkPostVotesAction() | |
{ | |
$this->logIn(MockUsers::PCH_USER); | |
$voter = static::$entityManager->getRepository('AvectraBundle:User')->findOneBy(array('firstName' => 'Long Beach Clicker')); | |
$manualBallotOptions = static::$entityManager->getRepository('VoteBundle:ManualBallotOption')->findAll(); | |
$json_data = array(); | |
foreach ($manualBallotOptions as $manualBallotOption) { | |
$json_data[] = array( | |
'manualBallotOption' => $manualBallotOption->getId(), | |
'user' => $this->avectraUserProvider->getPin($voter), | |
'vote' => rand(ManualPCHVote::SUPPORT, ManualPCHVote::OPPOSE) | |
); | |
} | |
static::$client->request( | |
'POST', | |
'/api/manualpchvotes.json', | |
array(), | |
array(), | |
array('CONTENT_TYPE' => 'application/json'), | |
json_encode($json_data) | |
); | |
$this->assertJsonResponse(static::$client->getResponse(), 200, false); | |
} | |
public function testJsonPostManualPCHVoteActionShouldReturn400() | |
{ | |
$this->logIn(MockUsers::PCH_USER); | |
$invalid_json_data = array( | |
'manualBallotOptions' => 2, | |
'users' => 1, | |
'vote' => ManualPCHVote::SUPPORT | |
); | |
static::$client->request( | |
'POST', | |
'/api/manualpchvotes.json', | |
array(), | |
array(), | |
array('CONTENT_TYPE' => 'application/json'), | |
$invalid_json_data | |
); | |
$this->assertJsonResponse(static::$client->getResponse(), 400, false); | |
} | |
protected function assertJsonResponse( | |
$response, | |
$statusCode = 200, | |
$checkValidJson = true, | |
$contentType = 'application/json' | |
) { | |
$this->assertEquals( | |
$statusCode, $response->getStatusCode(), | |
$response->getContent() | |
); | |
$this->assertTrue( | |
$response->headers->contains('Content-Type', $contentType), | |
$response->headers | |
); | |
if ($checkValidJson) { | |
$decode = json_decode($response->getContent()); | |
$this->assertTrue(($decode != null && $decode != false), | |
'is response valid json: [' . $response->getContent() . ']' | |
); | |
} | |
} | |
public function tearDown() | |
{ | |
static::$client->followRedirects(false); | |
$this->getContainer()->get('setting.manager')->set(Setting::VOTING_ENABLED, false); | |
parent::tearDown(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace ICC\VoteBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
use ICC\AvectraBundle\Entity\User; | |
use ICC\VoteBundle\Model\ManualVoteInterface; | |
use JMS\Serializer\Annotation as JMS; | |
/** | |
* ManualPCHVote | |
* | |
* @ORM\Entity | |
* @ORM\HasLifecycleCallbacks | |
* @ORM\Table(name="cdp_manual_pch_vote") | |
* @ORM\Entity(repositoryClass="ICC\VoteBundle\Entity\ManualPCHVoteRepository") | |
* @JMS\ExclusionPolicy("all") | |
*/ | |
class ManualPCHVote implements ManualVoteInterface | |
{ | |
const SUPPORT = 1; | |
const OPPOSE = 2; | |
/** | |
* @var int | |
* | |
* @ORM\Column(name="id", type="integer") | |
* @ORM\Id | |
* @ORM\GeneratedValue(strategy="AUTO") | |
* | |
* @JMS\Expose | |
* @JMS\Type("integer"); | |
*/ | |
protected $id; | |
/** | |
* @var ManualBallotOption | |
* | |
* @ORM\ManyToOne(targetEntity="ManualBallotOption") | |
* @ORM\JoinColumn(name="manual_ballot_option_id", referencedColumnName="id", onDelete="CASCADE") | |
*/ | |
protected $manualBallotOption; | |
/** | |
* @var User | |
* | |
* @ORM\ManyToOne(targetEntity="ICC\AvectraBundle\Entity\User") | |
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") | |
*/ | |
protected $user; | |
/** | |
* @var \DateTime | |
* | |
* @ORM\Column(name="created_at", type="datetime") | |
*/ | |
protected $createdAt; | |
/** | |
* @var \DateTime | |
* | |
* @ORM\Column(name="edited_at", type="datetime") | |
* | |
* @JMS\Expose | |
* @JMS\Type("DateTime") | |
* @JMS\Groups({"mycdpaccess"}) | |
*/ | |
protected $editedAt; | |
/** | |
* @var int | |
* @ORM\Column(name="vote", type="integer") | |
* SUPPORT(1) or OPPOSE(2) | |
* @JMS\Expose | |
* @JMS\Type("integer"); | |
*/ | |
protected $vote; | |
/** | |
* @param ManualBallotOption $manualBallotOption | |
* @param User $user | |
* @param int $vote | |
*/ | |
public function __construct(ManualBallotOption $manualBallotOption, User $user, $vote) | |
{ | |
$this->manualBallotOption = $manualBallotOption; | |
$this->user = $user; | |
$this->vote = $vote; | |
} | |
/** | |
* @ORM\PrePersist | |
*/ | |
public function setCreatedAtValue() | |
{ | |
$this->setCreatedAt(new \DateTime()); | |
$this->setEditedAt(new \DateTime()); | |
} | |
/** | |
* @ORM\PreUpdate | |
*/ | |
public function setPreUpdateValues() | |
{ | |
$this->setEditedAt(new \DateTime()); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getId() | |
{ | |
return $this->id; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getManualBallotOption() | |
{ | |
return $this->manualBallotOption; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getUser() | |
{ | |
return $this->user; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setCreatedAt($createdAt) | |
{ | |
$this->createdAt = $createdAt; | |
return $this; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getCreatedAt() | |
{ | |
return $this->createdAt; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setEditedAt($editedAt) | |
{ | |
$this->editedAt = $editedAt; | |
return $this; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getEditedAt() | |
{ | |
return $this->editedAt; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getVote() | |
{ | |
return $this->vote; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment