Skip to content

Instantly share code, notes, and snippets.

@Gorcenski
Last active December 27, 2022 17:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gorcenski/ac5e3f08c4302f30d4a5d877ec182a70 to your computer and use it in GitHub Desktop.
Save Gorcenski/ac5e3f08c4302f30d4a5d877ec182a70 to your computer and use it in GitHub Desktop.
Code for my Azure Function to syndicate links across multiple services
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