Instantly share code, notes, and snippets.
Last active
December 27, 2022 17:41
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save Gorcenski/ac5e3f08c4302f30d4a5d877ec182a70 to your computer and use it in GitHub Desktop.
Code for my Azure Function to syndicate links across multiple services
This file contains hidden or 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
namespace LinkSharer | |
open System | |
open System.Collections.Generic | |
open System.IO | |
open Microsoft.AspNetCore.Mvc | |
open Microsoft.Azure.WebJobs | |
open Microsoft.Azure.WebJobs.Extensions.Http | |
open Microsoft.AspNetCore.Http | |
open Newtonsoft.Json | |
open Microsoft.Extensions.Logging | |
open Google.Apis.Auth.OAuth2 | |
open Google.Apis.Sheets.v4 | |
open Google.Apis.Sheets.v4.Data | |
open Google.Apis.Services | |
open Mastonet | |
open Tweetinvi | |
module LinkSharer = | |
// Define a nullable container to deserialize into. | |
[<AllowNullLiteral>] | |
type NameContainer() = | |
member val Name = "" with get, set | |
// For convenience, it's better to have a central place for the literal. | |
[<Literal>] | |
let Name = "name" | |
[<Literal>] | |
let ApplicationName: string = "Interesting Links" | |
type LinkData = { | |
url: string | |
title: string | |
comment: string | |
} | |
type Result<'TSuccess,'TFailure> = | |
| Success of 'TSuccess | |
| Failure of 'TFailure | |
type Sheet = { service : SheetsService } | |
type Twitter = { client : TwitterClient } | |
type Mastodon = { mastodonClient: MastodonClient } | |
type ID = { id : string } | |
let bindAsync<'t,'s,'terr> (binder:'t -> Async<Result<'s,'terr>>) (result:Async<Result<'t,'terr>>) : Async<Result<'s,'terr>> = | |
async { | |
let! res = result | |
match res with | |
| Success s -> return! binder s | |
| Failure f -> return Failure f | |
} | |
let plusAsync addSuccess addFailure (switch1 : Async<Result<'s,'terr>>) (switch2 : Async<Result<'s,'terr>>) = | |
async { | |
let! res1 = switch1 | |
let! res2 = switch2 | |
return match (res1),(res2) with | |
| Success s1,Success s2 -> Success (addSuccess s1 s2) | |
| Failure f1,Success _ -> Failure f1 | |
| Success _ ,Failure f2 -> Failure f2 | |
| Failure f1,Failure f2 -> Failure (addFailure f1 f2) | |
} | |
let (>>@) twoTrackInput switchFunction = | |
bindAsync switchFunction twoTrackInput | |
let (>>==) v1 v2 = | |
let addSuccess r1 r2 = {id=r1.id + "; " + r2.id} | |
let addFailure s1 s2 = s1 + "; " + s2 // concat | |
plusAsync addSuccess addFailure v1 v2 | |
let buildPostFromData (data: LinkData) = | |
data.comment + "\n\n" + data.url | |
let getSheetService = | |
async { | |
let scopes: string list = [ SheetsService.Scope.Spreadsheets ] | |
let getServiceAccountCredential (googleCredential: GoogleCredential) = | |
googleCredential.CreateScoped(scopes).UnderlyingCredential | |
:?> ServiceAccountCredential | |
try | |
let credential : ServiceAccountCredential = | |
System.Environment.GetEnvironmentVariable("GOOGLE_SERVICE_ACCOUNT_CREDENTIAL") | |
|> Convert.FromBase64String | |
|> Text.Encoding.UTF8.GetString | |
|> GoogleCredential.FromJson | |
|> getServiceAccountCredential | |
let initializer: BaseClientService.Initializer = | |
new BaseClientService.Initializer(HttpClientInitializer=credential, | |
ApplicationName=ApplicationName) | |
return Success {service=new SheetsService(initializer)} | |
with | |
| ex -> return Failure ex.Message | |
} | |
let writeToGoogleSheet (data: LinkData) (input : Sheet) = | |
async { | |
let service: SheetsService = input.service | |
let sheetId: string = System.Environment.GetEnvironmentVariable("GOOGLE_SHEET_ID") | |
let range: string = "A:D" | |
let newItem: List<IList<Object>> = new List<IList<Object>>(); | |
let obj: List<Object> = new List<Object>([|data.url :> Object; | |
data.title; | |
DateTime.UtcNow; | |
data.comment|]); | |
newItem.Add(obj) | |
let request = | |
service.Spreadsheets.Values.Append(new ValueRange(Values=newItem), | |
sheetId, | |
range, | |
InsertDataOption=SpreadsheetsResource.ValuesResource.AppendRequest.InsertDataOptionEnum.INSERTROWS, | |
ValueInputOption=SpreadsheetsResource.ValuesResource.AppendRequest.ValueInputOptionEnum.USERENTERED) | |
try | |
let! response = request.ExecuteAsync() |> Async.AwaitTask | |
return Success {id=response.Updates.UpdatedRange} | |
with | |
| (ex: exn) -> return Failure ex.Message | |
} | |
let getTwitterClient = | |
async { | |
try | |
return Success {client=new TwitterClient(System.Environment.GetEnvironmentVariable("TWITTER_CONSUMER_KEY"), | |
System.Environment.GetEnvironmentVariable("TWITTER_CONSUMER_KEY_SECRET"), | |
System.Environment.GetEnvironmentVariable("TWITTER_ACCESS_TOKEN"), | |
System.Environment.GetEnvironmentVariable("TWITTER_ACCESS_TOKEN_SECRET"))} | |
with | |
| ex -> return Failure ex.Message | |
} | |
let writeTweet (data: LinkData) (input : Twitter) = | |
async { | |
let client = input.client | |
let tweet = buildPostFromData data | |
try | |
let post = client.Tweets.PublishTweetAsync(tweet) | |
return Success {id=post.Result.Id.ToString()} | |
with | |
| ex -> return Failure ex.Message | |
} | |
let getMastodonClient = | |
async { | |
try | |
let client = new MastodonClient(System.Environment.GetEnvironmentVariable("MASTODON_SERVER"), | |
System.Environment.GetEnvironmentVariable("MASTODON_ACCESS_TOKEN")) | |
return Success { mastodonClient=client } | |
with | |
| ex -> return Failure ex.Message | |
} | |
let writeToot (data: LinkData) (input: Mastodon) = | |
async { | |
let client = input.mastodonClient | |
let post = buildPostFromData data | |
try | |
let result = client.PublishStatus(post).Result | |
return Success { id=result.Id } | |
with | |
| ex -> return Failure ex.Message | |
} | |
let getPostFromReq (req: HttpRequest) = | |
async { | |
use stream: StreamReader = new StreamReader(req.Body) | |
let! (reqBody: string) = stream.ReadToEndAsync() |> Async.AwaitTask | |
let data: LinkData = JsonConvert.DeserializeObject<LinkData>(reqBody) | |
return data | |
} | |
[<FunctionName("LinkSharer")>] | |
let run ([<HttpTrigger(AuthorizationLevel.Function, "post", Route = null)>]req: HttpRequest) (log: ILogger) = | |
async { | |
log.LogInformation("F# HTTP trigger function processed a request.") | |
let! (data: LinkData) = getPostFromReq req | |
let tweet: Async<Result<ID,string>> = | |
getTwitterClient | |
>>@ writeTweet data | |
let sheet: Async<Result<ID,string>> = | |
getSheetService | |
>>@ writeToGoogleSheet data | |
let toot: Async<Result<ID,string>> = | |
getMastodonClient | |
>>@ writeToot data | |
let! (result: Result<ID,string>) = | |
tweet | |
>>== toot | |
>>== sheet | |
return match result with | |
| Success s -> OkObjectResult(s) :> IActionResult | |
| Failure f -> StatusCodeResult(500) :> IActionResult | |
} |> Async.StartAsTask |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment