diff --git a/src/Funogram.Generator/Types/TypesGenerator.fs b/src/Funogram.Generator/Types/TypesGenerator.fs
index d8328b5..fc51ca1 100644
--- a/src/Funogram.Generator/Types/TypesGenerator.fs
+++ b/src/Funogram.Generator/Types/TypesGenerator.fs
@@ -27,6 +27,7 @@ type ChatId =
type InputFile =
| Url of Uri
| File of string * Stream
+ | FileBytes of string * byte[]
| FileId of string
type ChatType =
diff --git a/src/Funogram.Telegram/Types.fs b/src/Funogram.Telegram/Types.fs
index abbc7b1..e4379be 100644
--- a/src/Funogram.Telegram/Types.fs
+++ b/src/Funogram.Telegram/Types.fs
@@ -11,6 +11,7 @@ type ChatId =
type InputFile =
| Url of Uri
| File of string * Stream
+ | FileBytes of string * byte[]
| FileId of string
type ChatType =
diff --git a/src/Funogram/Funogram.fsproj b/src/Funogram/Funogram.fsproj
index d72f2b4..18a1057 100644
--- a/src/Funogram/Funogram.fsproj
+++ b/src/Funogram/Funogram.fsproj
@@ -3,7 +3,7 @@
netstandard2.0
- 2.0.9
+ 2.0.10
Nikolay Matyushin
Funogram
Funogram is a functional Telegram Bot Api library for F#
diff --git a/src/Funogram/Resolvers.fs b/src/Funogram/Resolvers.fs
index 0b48bba..eeba3e7 100644
--- a/src/Funogram/Resolvers.fs
+++ b/src/Funogram/Resolvers.fs
@@ -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; typeof|]
+ let isFile =
+ case.Fields.Length = 2
+ && case.Fields[0].Member.Type = typeof
+ && (case.Fields[1].Member.Type = typeof || case.Fields[1].Member.Type = typeof)
+
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()
diff --git a/src/Funogram/Tools.fs b/src/Funogram/Tools.fs
index 5e02e4c..95841f8 100644
--- a/src/Funogram/Tools.fs
+++ b/src/Funogram/Tools.fs
@@ -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
@@ -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
@@ -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;typeof|]
+ let isFileStream (case: ShapeFSharpUnionCase<'T>) =
+ case.Fields.Length = 2 && case.Fields[0].Member.Type = typeof && case.Fields[1].Member.Type = typeof
+
+ let isFileBytes (case: ShapeFSharpUnionCase<'T>) =
+ case.Fields.Length = 2 && case.Fields[0].Member.Type = typeof && case.Fields[1].Member.Type = typeof
- 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()
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
@@ -149,14 +174,14 @@ 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)
}
@@ -164,7 +189,7 @@ module Api =
| 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>
@@ -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 }
@@ -188,13 +213,13 @@ 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
@@ -202,15 +227,17 @@ module Api =
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)
@@ -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
@@ -253,7 +280,11 @@ module Api =
fileFinders.GetOrAdd(typeof<'v>, Func(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)
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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
diff --git a/src/examples/Funogram.TestBot/Commands/Base.fs b/src/examples/Funogram.TestBot/Commands/Base.fs
index 004350c..8ad9732 100644
--- a/src/examples/Funogram.TestBot/Commands/Base.fs
+++ b/src/examples/Funogram.TestBot/Commands/Base.fs
@@ -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
@@ -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)
diff --git a/src/examples/Funogram.TestBot/Commands/Files.fs b/src/examples/Funogram.TestBot/Commands/Files.fs
index 23c5d13..41f0392 100644
--- a/src/examples/Funogram.TestBot/Commands/Files.fs
+++ b/src/examples/Funogram.TestBot/Commands/Files.fs
@@ -1,5 +1,6 @@
module Funogram.TestBot.Commands.Files
+open System.IO
open FunHttp
open Funogram.Telegram
open Funogram.Telegram.Types
@@ -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