Skip to content

Commit

Permalink
Add support for InputFile from byte array (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dolfik1 authored Jan 31, 2024
1 parent 3dd1982 commit 780e772
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 38 deletions.
1 change: 1 addition & 0 deletions src/Funogram.Generator/Types/TypesGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ChatId =
type InputFile =
| Url of Uri
| File of string * Stream
| FileBytes of string * byte[]
| FileId of string
type ChatType =
Expand Down
1 change: 1 addition & 0 deletions src/Funogram.Telegram/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ChatId =
type InputFile =
| Url of Uri
| File of string * Stream
| FileBytes of string * byte[]
| FileId of string

type ChatType =
Expand Down
2 changes: 1 addition & 1 deletion src/Funogram/Funogram.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>2.0.9</VersionPrefix>
<VersionPrefix>2.0.10</VersionPrefix>
<Authors>Nikolay Matyushin</Authors>
<Product>Funogram</Product>
<Description>Funogram is a functional Telegram Bot Api library for F#</Description>
Expand Down
10 changes: 7 additions & 3 deletions src/Funogram/Resolvers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ module internal Resolvers =
else yield c
}
String.Concat(chars).ToLower()

let mkMemberSerializer (case: ShapeFSharpUnionCase<'DeclaringType>) =
let isFile = case.Fields |> Array.map (fun x -> x.Member.Type) = [|typeof<string>; typeof<Stream>|]
let isFile =
case.Fields.Length = 2
&& case.Fields[0].Member.Type = typeof<string>
&& (case.Fields[1].Member.Type = typeof<Stream> || case.Fields[1].Member.Type = typeof<byte[]>)

if case.Fields.Length = 0 then
fun _ _ -> Encoding.UTF8.GetBytes(getSnakeCaseName case.CaseInfo.Name |> sprintf "\"%s\"")
else
case.Fields.[0].Accept { new IMemberVisitor<'DeclaringType, 'DeclaringType -> IJsonFormatterResolver -> byte[]> with
member __.Visit (shape : ShapeMember<'DeclaringType, 'Field>) =
member _.Visit (shape : ShapeMember<'DeclaringType, 'Field>) =
fun value resolver ->
let mutable myWriter = JsonWriter()

Expand Down
105 changes: 71 additions & 34 deletions src/Funogram/Tools.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open System.Net
open System.Net.Http
open System.Runtime.CompilerServices
open System.Text
open Funogram.Types
open Utf8Json
open Utf8Json.Resolvers
Expand All @@ -15,7 +16,6 @@ do ()
open System.Collections.Concurrent
open System.IO
open System.Linq.Expressions
open Funogram.Types
open Funogram.Resolvers
open TypeShape.Core
open Utf8Json.FSharp
Expand Down Expand Up @@ -111,36 +111,61 @@ let toJsonBotRequest (request: IBotRequest) =
toJson.Invoke(request)

module Api =
type File = string * Stream
type File =
| Stream of string * Stream
| Bytes of string * byte[]

let isFile (case: ShapeFSharpUnionCase<'T>) =
case.Fields
|> Array.map (fun x -> x.Member.Type) = [|typeof<string>;typeof<Stream>|]
let isFileStream (case: ShapeFSharpUnionCase<'T>) =
case.Fields.Length = 2 && case.Fields[0].Member.Type = typeof<string> && case.Fields[1].Member.Type = typeof<Stream>

let isFileBytes (case: ShapeFSharpUnionCase<'T>) =
case.Fields.Length = 2 && case.Fields[0].Member.Type = typeof<string> && case.Fields[1].Member.Type = typeof<byte[]>

let readFile =
let readFileStream =
fun (x: 'T) (case: ShapeFSharpUnionCase<'T>) ->
let a =
case.Fields.[0].Accept {
case.Fields[0].Accept {
new IMemberVisitor<'T, 'T -> string> with
member __.Visit (shape : ShapeMember<'T, 'a>) =
member _.Visit (shape : ShapeMember<'T, 'a>) =
let cast c = (box c) :?> string
shape.Get >> cast
}

let b =
case.Fields.[1].Accept {
case.Fields[1].Accept {
new IMemberVisitor<'T, 'T -> Stream> with
member __.Visit (shape : ShapeMember<'T, 'b>) =
member _.Visit (shape : ShapeMember<'T, 'b>) =
let cast c = (box c) :?> Stream
shape.Get >> cast
}
(a x, b x)

File.Stream (a x, b x)

let readFileBytes =
fun (x: 'T) (case: ShapeFSharpUnionCase<'T>) ->
let a =
case.Fields[0].Accept {
new IMemberVisitor<'T, 'T -> string> with
member _.Visit (shape : ShapeMember<'T, 'a>) =
let cast c = (box c) :?> string
shape.Get >> cast
}

let b =
case.Fields[1].Accept {
new IMemberVisitor<'T, 'T -> byte[]> with
member _.Visit (shape : ShapeMember<'T, 'b>) =
let cast c = (box c) :?> byte[]
shape.Get >> cast
}

File.Bytes (a x, b x)

let fileFinders = ConcurrentDictionary<Type, obj>()
let rec mkFilesFinder<'T> () : 'T -> File[] =
let mkMemberFinder (shape : IShapeMember<'T>) =
shape.Accept { new IMemberVisitor<'T, 'T -> File[]> with
member __.Visit (shape : ShapeMember<'T, 'a>) =
member _.Visit (shape : ShapeMember<'T, 'a>) =
let fieldFinder = mkFilesFinder<'a>()
fieldFinder << shape.Get }
let wrap(p : 'a -> File[]) = unbox<'T -> File[]> p
Expand All @@ -149,22 +174,22 @@ module Api =
| Shape.FSharpOption s ->
s.Element.Accept {
new ITypeVisitor<'T -> File[]> with
member __.Visit<'a> () =
member _.Visit<'a> () =
let tp = mkFilesFinder<'a>()
wrap(function None -> [||] | Some t -> (tp t))
}
| Shape.FSharpList s ->
s.Element.Accept {
new ITypeVisitor<'T -> File[]> with
member __.Visit<'a> () =
member _.Visit<'a> () =
let tp = mkFilesFinder<'a>()
wrap(fun ts -> ts |> Seq.map tp |> Array.concat)
}

| Shape.Array s when s.Rank = 1 ->
s.Element.Accept {
new ITypeVisitor<'T -> File[]> with
member __.Visit<'a> () =
member _.Visit<'a> () =
let tp = mkFilesFinder<'a> ()
fun (t: 'T) ->
let r = t |> box :?> seq<'a>
Expand All @@ -174,7 +199,7 @@ module Api =
| Shape.Tuple (:? ShapeTuple<'T> as shape) ->
let mkElemFinder (shape : IShapeMember<'T>) =
shape.Accept { new IMemberVisitor<'T, 'T -> File[]> with
member __.Visit (shape : ShapeMember<'T, 'Field>) =
member _.Visit (shape : ShapeMember<'T, 'Field>) =
let fieldFinder = mkFilesFinder<'Field>()
fieldFinder << shape.Get }

Expand All @@ -188,29 +213,31 @@ module Api =
| Shape.FSharpSet s ->
s.Accept {
new IFSharpSetVisitor<'T -> File[]> with
member __.Visit<'a when 'a : comparison> () =
member _.Visit<'a when 'a : comparison> () =
let tp = mkFilesFinder<'a>()
wrap(fun (s:Set<'a>) -> s |> Seq.map tp |> Array.concat)
}
| Shape.FSharpRecord (:? ShapeFSharpRecord<'T> as shape) ->
let fieldPrinters : ('T -> File[]) [] =
shape.Fields |> Array.map (fun f -> mkMemberFinder f)
shape.Fields |> Array.map mkMemberFinder

fun (r:'T) ->
fieldPrinters |> Seq.map (fun fp -> fp r) |> Array.concat
| Shape.FSharpUnion (:? ShapeFSharpUnion<'T> as shape) ->
let cases : ShapeFSharpUnionCase<'T> [] = shape.UnionCases // all union cases
let mkUnionCasePrinter (case : ShapeFSharpUnionCase<'T>) =
let readFile =
if isFile case then
readFile |> Some
if isFileStream case then
readFileStream |> Some
elif isFileBytes case then
readFileBytes |> Some
else None

let fieldPrinters = case.Fields |> Array.map mkMemberFinder
fun (x: 'T) ->
match readFile with
| Some fn ->
[|fn x case|]
[| fn x case |]
| None ->
fieldPrinters
|> Seq.map (fun fp -> fp x)
Expand Down Expand Up @@ -240,10 +267,10 @@ module Api =
let inline ($) _ x = x

let mkGenerateInMember (shape : IShapeMember<'DeclaringType>) =
shape.Accept { new IMemberVisitor<'DeclaringType, 'DeclaringType -> string -> MultipartFormDataContent -> bool> with
member __.Visit (shape : ShapeMember<'DeclaringType, 'Field>) =
let inFieldFinder = mkRequestGenerator<'Field>()
inFieldFinder << shape.Get }
shape.Accept { new IMemberVisitor<'DeclaringType, 'DeclaringType -> string -> MultipartFormDataContent -> bool> with
member _.Visit (shape : ShapeMember<'DeclaringType, 'Field>) =
let inFieldFinder = mkRequestGenerator<'Field>()
inFieldFinder << shape.Get }

let wrap(p : 'a -> string -> MultipartFormDataContent -> bool) =
unbox<'T -> string -> MultipartFormDataContent -> bool> p
Expand All @@ -253,7 +280,11 @@ module Api =
fileFinders.GetOrAdd(typeof<'v>, Func<Type, obj>(fun x -> mkFilesFinder<'v> () |> box))
|> unbox<'v -> File[]>
let files = finder a
files |> Seq.iter (fun (name, stream) -> data.Add(new StreamContent(stream), name, name))
files |> Seq.iter (fun x ->
match x with
| File.Stream (name, stream) -> data.Add(new StreamContent(stream), name, name)
| File.Bytes (name, bytes) -> data.Add(new ByteArrayContent(bytes), name, name)
)

let strf a b = new StringContent(sprintf a b)

Expand Down Expand Up @@ -303,7 +334,7 @@ module Api =
| Shape.FSharpOption s ->
s.Element.Accept {
new ITypeVisitor<'T -> string -> MultipartFormDataContent -> bool> with
member __.Visit<'a> () =
member _.Visit<'a> () =
let tp = mkRequestGenerator<'a>()
wrap(fun x prop data ->
match x with
Expand All @@ -313,7 +344,7 @@ module Api =
| Shape.FSharpList s ->
s.Element.Accept {
new ITypeVisitor<'T -> string -> MultipartFormDataContent -> bool> with
member __.Visit<'a> () =
member _.Visit<'a> () =
fun x prop data ->
let json = toJson x
data.Add(new ByteArrayContent(json), prop)
Expand All @@ -323,7 +354,7 @@ module Api =
| Shape.Array s when s.Rank = 1 ->
s.Element.Accept {
new ITypeVisitor<'T -> string -> MultipartFormDataContent -> bool> with
member __.Visit<'a> () =
member _.Visit<'a> () =
fun x prop data ->
let json = toJson x
data.Add(new ByteArrayContent(json), prop)
Expand All @@ -333,7 +364,7 @@ module Api =
| Shape.FSharpSet s ->
s.Accept {
new IFSharpSetVisitor<'T -> string -> MultipartFormDataContent -> bool> with
member __.Visit<'a when 'a : comparison> () =
member _.Visit<'a when 'a : comparison> () =
fun x prop data ->
let json = toJson x
data.Add(new ByteArrayContent(json), prop)
Expand All @@ -346,8 +377,10 @@ module Api =
let isEnum = case.Fields.Length = 0

let readFile =
if isFile case then
readFile |> Some
if isFileStream case then
readFileStream |> Some
elif isFileBytes case then
readFileBytes |> Some
else None

if isEnum then
Expand All @@ -358,8 +391,12 @@ module Api =
fun (x: 'T) (prop: string) (data: MultipartFormDataContent) ->
match readFile with
| Some fn ->
let (n, s) = fn x case
data.Add(new StreamContent(s), prop, n) $ true
let file = fn x case
match file with
| Stream (name, stream) ->
data.Add(new StreamContent(stream), prop, name) $ true
| Bytes (name, bytes) ->
data.Add(new ByteArrayContent(bytes), prop, name) $ true
| None ->
let fieldPrinters = case.Fields |> Array.map mkGenerateInMember
fieldPrinters
Expand Down
2 changes: 2 additions & 0 deletions src/examples/Funogram.TestBot/Commands/Base.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let defaultText = """⭐️Available test commands:
/send_message6 - Test RemoveKeyboardMarkup
/send_message7 - Test inline keyboard
/send_message8 - Test multiple media
/send_message9 - Test multiple media as bytes
/send_action - Test action
Expand Down Expand Up @@ -43,6 +44,7 @@ let updateArrived (ctx: UpdateContext) =
cmd "/send_message7" (fun _ -> Markup.testInlineKeyboard |> wrap)

cmd "/send_message8" (fun _ -> Files.testUploadAndSendPhotoGroup |> wrap)
cmd "/send_message9" (fun _ -> Files.testUploadAndSendPhotoGroupAsBytes |> wrap)

cmd "/forward_message" (fun _ -> TextMessages.testForwardMessage ctx |> wrap)

Expand Down
26 changes: 26 additions & 0 deletions src/examples/Funogram.TestBot/Commands/Files.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Funogram.TestBot.Commands.Files

open System.IO
open FunHttp
open Funogram.Telegram
open Funogram.Telegram.Types
Expand Down Expand Up @@ -27,3 +28,28 @@ let testUploadAndSendPhotoGroup config (chatId: int64) =
let testUploadAndSendSinglePhoto config (chatId: int64) =
let image = Http.RequestStream(PhotoUrl)
Req.SendPhoto.Make(chatId, InputFile.File("example.jpg", image.ResponseStream), caption = "Example") |> bot config

let testUploadAndSendPhotoGroupAsBytes config (chatId: int64) =
let pack name bytes =
{
InputMediaPhoto.Media = InputFile.FileBytes(name, bytes)
Type = "photo"
Caption = Some name
ParseMode = None
CaptionEntities = None
HasSpoiler = None
} |> InputMedia.Photo

use image1 = Http.RequestStream(PhotoUrl).ResponseStream
use image2 = Http.RequestStream(PhotoUrl).ResponseStream

use ms1 = new MemoryStream()
image1.CopyTo(ms1)
ms1.Seek(0, SeekOrigin.Begin) |> ignore

use ms2 = new MemoryStream()
image2.CopyTo(ms2)
ms2.Seek(0, SeekOrigin.Begin) |> ignore

let media = [| pack "Image.jpg" (ms1.ToArray()); pack "Image.jpg" (ms2.ToArray()) |]
Req.SendMediaGroup.Make(chatId, media) |> bot config

0 comments on commit 780e772

Please sign in to comment.