Skip to content

Commit

Permalink
Send a tweet when a new package is releases
Browse files Browse the repository at this point in the history
  • Loading branch information
aaubry committed Mar 30, 2021
1 parent 188d9dc commit cb0768c
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 2 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ jobs:
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }}
TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }}
run: ./build.sh ${{ github.event.inputs.target }}
8 changes: 8 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ test: off
environment:
NUGET_API_KEY:
secure: pomYNeSluKjzaqIYea800dn9dd/CZg5AOSJQnkpjFFxNO13qcA2l6KW/6dTxfQEb
TWITTER_CONSUMER_API_KEY:
secure: o4jM6NnXqtI3/Bzef2xpcTAOuPn/WwO+khiWvrbletk=
TWITTER_CONSUMER_API_SECRET:
secure: a4TQjB1vp0lRFZ7aA7RamHAhkHlPlMqdvsaZvzGs/pPkR+YfuGQ7AKlknD26HIE0xGUTi8M1wqffQUKJL1knHg==
TWITTER_ACCESS_TOKEN:
secure: TfgxKzf9AAbDuJmuYnnzjYKhHcxyYv7Z+AnFRzlQc5CcIJ3h52YZAokQGF3n3K8r21V1DxeKHc/qWG1yZs4NKg==
TWITTER_ACCESS_TOKEN_SECRET:
secure: aMvxmavbq8tylMDe8QGEdemNMPzrd7i6982i/iVydNYHhUvzV7lw5AJsiEpfRyzt

install:
- cmd: git submodule update --init
Expand Down
24 changes: 22 additions & 2 deletions tools/build/BuildDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ public static void Publish(Options options, GitVersion version, NuGetPackage pac
}
}

public static async Task TweetRelease(GitVersion version)
{
var twitterClient = new TwitterProvider(
consumerKey: Environment.GetEnvironmentVariable("TWITTER_CONSUMER_API_KEY") ?? throw new InvalidOperationException("Please set the TWITTER_CONSUMER_API_KEY environment variable."),
consumerKeySecret: Environment.GetEnvironmentVariable("TWITTER_CONSUMER_API_SECRET") ?? throw new InvalidOperationException("Please set the TWITTER_CONSUMER_API_SECRET environment variable."),
accessToken: Environment.GetEnvironmentVariable("TWITTER_ACCESS_TOKEN") ?? throw new InvalidOperationException("Please set the TWITTER_ACCESS_TOKEN environment variable."),
accessTokenSecret: Environment.GetEnvironmentVariable("TWITTER_ACCESS_TOKEN_SECRET") ?? throw new InvalidOperationException("Please set the TWITTER_ACCESS_TOKEN_SECRET environment variable.")
);

var message = $"YamlDotNet {version.NuGetVersion} has just been released! https://github.com/aaubry/YamlDotNet/releases/tag/v{version.NuGetVersion}";
var result = await twitterClient.Tweet(message);
WriteVerbose(result);
}

public static ScaffoldedRelease ScaffoldReleaseNotes(GitVersion version, PreviousReleases releases)
{
if (version.IsPreRelease)
Expand Down Expand Up @@ -459,12 +473,18 @@ internal class LoggerHttpHandler : HttpClientHandler
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var requestText = await request.Content.ReadAsStringAsync();
WriteVerbose($"> {request.Method} {request.RequestUri}\n{requestText}\n".Replace("\n", "\n> "));
var requestHeaders = request.Headers.Concat(request.Content.Headers)
.Select(h => $"\n{h.Key}: {string.Join(", ", h.Value)}");

WriteVerbose($"> {request.Method} {request.RequestUri}{string.Concat(requestHeaders)}\n\n{requestText}\n".Replace("\n", "\n> "));

var response = await base.SendAsync(request, cancellationToken);

var responseText = await response.Content.ReadAsStringAsync();
WriteVerbose($"< {response.StatusCode}\n{responseText}\n".Replace("\n", "\n< "));
var responseHeaders = response.Headers.Concat(response.Content.Headers)
.Select(h => $"\n{h.Key}: {string.Join(", ", h.Value)}");

WriteVerbose($"< {(int)response.StatusCode} {response.ReasonPhrase}{string.Concat(responseHeaders)}\n\n{responseText}\n".Replace("\n", "\n< "));

return response;
}
Expand Down
1 change: 1 addition & 0 deletions tools/build/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ static int Main(string[] args)
WriteInformation("Publishable build detected");
targets.Add(nameof(BuildDefinition.SetBuildVersion));
targets.Add(nameof(BuildDefinition.Publish));
targets.Add(nameof(BuildDefinition.TweetRelease));
}
else
{
Expand Down
136 changes: 136 additions & 0 deletions tools/build/TwitterProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Stolen from https://github.com/cake-contrib/Cake.Twitter

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace build
{
// The code within this TwitterProvider has been based almost exclusively on the work that was done by Danny Tuppeny
// based on this blog post:
// https://blog.dantup.com/2016/07/simplest-csharp-code-to-post-a-tweet-using-oauth/
/// <summary>
/// Contains functionality related to Twitter API
/// </summary>
internal sealed class TwitterProvider
{
const string TwitterApiBaseUrl = "https://api.twitter.com/1.1/";
readonly string consumerKey, consumerKeySecret, accessToken, accessTokenSecret;
readonly HMACSHA1 sigHasher;
readonly DateTime epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// Creates an object for sending tweets to Twitter using Single-user OAuth.
///
/// Get your access keys by creating an app at apps.twitter.com then visiting the
/// "Keys and Access Tokens" section for your app. They can be found under the
/// "Your Access Token" heading.
/// </summary>
public TwitterProvider(string consumerKey, string consumerKeySecret, string accessToken, string accessTokenSecret)
{
this.consumerKey = consumerKey;
this.consumerKeySecret = consumerKeySecret;
this.accessToken = accessToken;
this.accessTokenSecret = accessTokenSecret;

sigHasher = new HMACSHA1(new ASCIIEncoding().GetBytes(string.Format("{0}&{1}", consumerKeySecret, accessTokenSecret)));
}

/// <summary>
/// Sends a tweet with the supplied text and returns the response from the Twitter API.
/// </summary>
public Task<string> Tweet(string text)
{
var data = new Dictionary<string, string> {
{ "status", text },
{ "trim_user", "1" }
};

return SendRequest("statuses/update.json", data);
}

Task<string> SendRequest(string url, Dictionary<string, string> data)
{
var fullUrl = TwitterApiBaseUrl + url;

// Timestamps are in seconds since 1/1/1970.
var timestamp = (int)((DateTime.UtcNow - epochUtc).TotalSeconds);

// Add all the OAuth headers we'll need to use when constructing the hash.
data.Add("oauth_consumer_key", consumerKey);
data.Add("oauth_signature_method", "HMAC-SHA1");
data.Add("oauth_timestamp", timestamp.ToString());
data.Add("oauth_nonce", "a"); // Required, but Twitter doesn't appear to use it, so "a" will do.
data.Add("oauth_token", accessToken);
data.Add("oauth_version", "1.0");

// Generate the OAuth signature and add it to our payload.
data.Add("oauth_signature", GenerateSignature(fullUrl, data));

// Build the OAuth HTTP Header from the data.
string oAuthHeader = GenerateOAuthHeader(data);

// Build the form data (exclude OAuth stuff that's already in the header).
var formData = new FormUrlEncodedContent(data.Where(kvp => !kvp.Key.StartsWith("oauth_")));

return SendRequest(fullUrl, oAuthHeader, formData);
}

/// <summary>
/// Generate an OAuth signature from OAuth header values.
/// </summary>
string GenerateSignature(string url, Dictionary<string, string> data)
{
var sigString = string.Join(
"&",
data
.Union(data)
.Select(kvp => string.Format("{0}={1}", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);

var fullSigData = string.Format(
"{0}&{1}&{2}",
"POST",
Uri.EscapeDataString(url),
Uri.EscapeDataString(sigString.ToString())
);

return Convert.ToBase64String(sigHasher.ComputeHash(new ASCIIEncoding().GetBytes(fullSigData.ToString())));
}

/// <summary>
/// Generate the raw OAuth HTML header from the values (including signature).
/// </summary>
string GenerateOAuthHeader(Dictionary<string, string> data)
{
return "OAuth " + string.Join(
", ",
data
.Where(kvp => kvp.Key.StartsWith("oauth_"))
.Select(kvp => string.Format("{0}=\"{1}\"", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);
}

/// <summary>
/// Send HTTP Request and return the response.
/// </summary>
async Task<string> SendRequest(string fullUrl, string oAuthHeader, FormUrlEncodedContent formData)
{
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Add("Authorization", oAuthHeader);

var httpResp = await http.PostAsync(fullUrl, formData);
var respBody = await httpResp.Content.ReadAsStringAsync();

return respBody;
}
}
}
}

0 comments on commit cb0768c

Please sign in to comment.