Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for InputFile from byte array #70

Merged
merged 4 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![.NET Core](https://github.com/Dolfik1/Funogram/workflows/.NET/badge.svg)
[![NuGet](https://img.shields.io/nuget/v/Funogram.svg)](https://www.nuget.org/packages/Funogram/)
[![NuGet](https://img.shields.io/nuget/v/Funogram.Telegram.svg)](https://www.nuget.org/packages/Funogram.Telegram/)
[![NuGet](https://img.shields.io/badge/Bot%20API-6.7-blue?logo=telegram)](https://www.nuget.org/packages/Funogram.Telegram/)
[![NuGet](https://img.shields.io/badge/Bot%20API-7.0-blue?logo=telegram)](https://www.nuget.org/packages/Funogram.Telegram/)

<img src="https://github.com/Dolfik1/Funogram/raw/master/docs/files/img/logo.png" alt="Funogram Logo" width="200" align="right" />

Expand Down
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
2 changes: 1 addition & 1 deletion src/Funogram.Telegram/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>7.0.0.0</Version>
<Version>7.0.0.1</Version>
<Authors>Nikolay Matyushin</Authors>
<Product>Funogram.Telegram</Product>
<Title>Funogram.Telegram</Title>
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
Loading