Add initial version of the irc-push Azure Function

This relies on spawning a thread for dispatching things to IRC, since GitHub
webhooks will timeout in 10s
    https://github.community/t5/GitHub-API-Development-and/Extended-Service-Timeout-for-webhooks/td-p/19639

Really though, it's not like webhook logs are terribly useful in GitHub's VUI
This commit is contained in:
R Tyler Croy 2020-01-12 13:22:06 -08:00
commit 4281b536db
13 changed files with 557 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
target/
Cargo.lock
.vscode/
.git/
.cargo/

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
**/*.rs.bk
*.swp
package-lock.json
node_modules/
.cargo/

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "attach",
"name": "Debug",
"windows": {
"program": "irc-push.exe"
},
"program": "irc-push",
"preLaunchTask": "Launch Azure Functions Application",
"postDebugTask": "Terminate Azure Functions Application"
}
]
}

46
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,46 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Launch Azure Functions Application",
"type": "shell",
"command": "cargo",
"args": [
"func",
"run"
],
"presentation": {
"reveal": "always",
"clear": true,
"focus": true
},
"problemMatcher": [
{
"owner": "azureFunctions",
"pattern": [
{
"regexp": "\\b\\B",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^Azure Functions Core Tools",
"endsPattern": "^Application started."
}
}
],
"isBackground": true
},
{
"label": "Terminate Azure Functions Application",
"type": "process",
"command":"${command:workbench.action.tasks.terminate}",
"args": [
"Launch Azure Functions Application"
]
}
]
}

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "irc-push"
version = "0.1.0"
authors = ["tyler"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
azure-functions = "0.11.0"
irc = "~0.13.6"
log = "~0.4.8"
pretty_env_logger = "~0.3.1"
# Needed for deserializing objects
serde = "~1.0.103"
serde_json = "~1.0.0"

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# syntax=docker/dockerfile-upstream:experimental
FROM peterhuene/azure-functions-rs-build:0.11.0 AS build-image
WORKDIR /src
COPY . /src
RUN apt-get install -qy libssl-dev pkg-config
# Run with mounted cache
RUN --mount=type=cache,target=/src/target \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/usr/local/cargo/registry \
["cargo", "run", "--release", "--", "init", "--script-root", "/home/site/wwwroot", "--sync-extensions"]
FROM mcr.microsoft.com/azure-functions/base:2.0 as runtime-image
FROM mcr.microsoft.com/dotnet/core/runtime-deps:2.2
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
HOME=/home \
FUNCTIONS_WORKER_RUNTIME=Rust \
languageWorkers__workersDirectory=/home/site/wwwroot/workers
# Copy the Azure Functions host from the runtime image
COPY --from=runtime-image [ "/azure-functions-host", "/azure-functions-host" ]
# Copy the script root contents from the build image
COPY --from=build-image ["/home/site/wwwroot", "/home/site/wwwroot"]
WORKDIR /home/site/wwwroot
CMD [ "/azure-functions-host/Microsoft.Azure.WebJobs.Script.WebHost" ]

17
README.adoc Normal file
View File

@ -0,0 +1,17 @@
= IRC push function
== Development
=== Local invocations
[source,bash]
----
curl -XPOST --data @events.push.json http://localhost:8100/api/webhook
----
=== Deployment
[source,bash]
----
docker build -t functionsbrokencode.azurecr.io/irc-push . && docker push functionsbrokencode.azurecr.io/irc-push
----

136
events.push.json Normal file
View File

@ -0,0 +1,136 @@
{
"ref": "refs/tags/simple-tag",
"before": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"after": "0000000000000000000000000000000000000000",
"created": false,
"deleted": true,
"forced": false,
"base_ref": null,
"compare": "https://github.com/Codertocat/Hello-World/compare/6113728f27ae...000000000000",
"commits": [
],
"head_commit": null,
"repository": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com",
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://github.com/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": 1557933565,
"updated_at": "2019-05-15T15:20:41Z",
"pushed_at": 1557933657,
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Ruby",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 1,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 1,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"stargazers": 0,
"master_branch": "master"
},
"pusher": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com"
},
"sender": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
}
}

8
host.json Normal file
View File

@ -0,0 +1,8 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Information"
}
}
}

9
local.settings.json Normal file
View File

@ -0,0 +1,9 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "Rust",
"languageWorkers:workersDirectory": "workers"
},
"ConnectionStrings": {
}
}

8
src/functions/mod.rs Normal file
View File

@ -0,0 +1,8 @@
// WARNING: This file is regenerated by the `cargo func new` command.
mod webhook;
// Export the Azure Functions here.
azure_functions::export! {
webhook::webhook,
}

252
src/functions/webhook.rs Normal file
View File

@ -0,0 +1,252 @@
extern crate irc;
extern crate serde;
extern crate serde_json;
use azure_functions::{
bindings::{HttpRequest, HttpResponse},
func,
};
use irc::client::prelude::*;
use log::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::default::Default;
use std::thread;
fn dispatch_to_irc(event: PushEvent) {
thread::spawn(move || {
/*
* Currently uncertain on how to import a file
*/
//let config = Config::load("Irc.toml").expect("failed to load Irc.toml");
let config = Config {
nickname: Some("otto-gh-bot".to_owned()),
server: Some("chat.freenode.net".to_owned()),
channels: Some(vec!["#otto".to_owned()]),
..Default::default()
};
/*
* Grabbing a clone of our configured channels before the config
* moves into IrcClient
*/
let channels = &config.channels.clone();
let client = IrcClient::from_config(config)
.expect("Failed to construct the IrcClient");
client.identify().unwrap();
client.for_each_incoming(|irc_msg| {
let m = irc_msg.to_string();
/*
* This effectively means the channel has been synchronized and we can
* send the message we need to send
*/
if m.contains("End of /NAMES list") {
if let Some(channel) = channels {
for chan in channel.iter() {
info!("Sending message to {}", chan);
client.send_privmsg(chan,
format!("{} pushed {} commits to {} in {} <{}>",
&event.sender.login,
&event.commits.len(),
&event.refname,
&event.repository.full_name,
&event.compare))
.expect(format!("Failed to send message to channel: {}", chan).as_str());
client.send_quit("rusty , over and out")
.expect("Failed to send QUIT");
}
}
}
}).unwrap();
});
}
#[func]
pub fn webhook(req: HttpRequest) -> HttpResponse {
if let Ok(event) = req.body().as_json::<PushEvent>() {
info!("Parsed push event from {}", event.sender.login);
dispatch_to_irc(event);
return "Messages dispatched to IRC".into();
}
else {
return "Failed to parse event".into();
}
}
#[derive(Serialize, Deserialize)]
struct Sender {
login: String,
}
#[derive(Serialize, Deserialize)]
struct Repo {
name: String,
full_name: String,
}
#[derive(Serialize, Deserialize)]
struct PushEvent {
#[serde(rename = "ref")]
refname: String,
sender: Sender,
compare: String,
commits: Vec<Value>,
repository: Repo,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_push_event() {
/*
* Example event taken from:
* <https://developer.github.com/v3/activity/events/types/#pushevent>
*/
let push = r#"{
"ref": "refs/tags/simple-tag",
"before": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"after": "0000000000000000000000000000000000000000",
"created": false,
"deleted": true,
"forced": false,
"base_ref": null,
"compare": "https://github.com/Codertocat/Hello-World/compare/6113728f27ae...000000000000",
"commits": [
],
"head_commit": null,
"repository": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com",
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://github.com/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": 1557933565,
"updated_at": "2019-05-15T15:20:41Z",
"pushed_at": 1557933657,
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Ruby",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 1,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 1,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"stargazers": 0,
"master_branch": "master"
},
"pusher": {
"name": "Codertocat",
"email": "21031067+Codertocat@users.noreply.github.com"
},
"sender": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
}
}
"#;
let event = serde_json::from_str::<PushEvent>(push).expect("Failed to deserialized event");
assert_eq!("Codertocat", event.sender.login);
assert_eq!("refs/tags/simple-tag", event.refname);
}
}

5
src/main.rs Normal file
View File

@ -0,0 +1,5 @@
mod functions;
fn main() {
azure_functions::worker_main(std::env::args(), functions::EXPORTS);
}