I'm trying out the Twilio service to interact with individuals via SMS/MMS. I've sorta figured out how to send MMS messages to initiate the "conversation" and that seems to be working well. However, now I'm trying to build a system to respond to the incoming messages on my SMS/MMS-enabled test number. I'm working from one of the examples I found on the Twilio documentation site to build an ASP.NET MVC web service to handle the conversation (VB.NET):
Imports System.Web.Mvc
Imports Twilio.AspNet.Common
Imports Twilio.AspNet.Mvc
Imports Twilio.TwiML
Namespace Controllers
Public Class SMSController
Inherits TwilioController
' GET: SMS
Function Index(ByVal IncomingSMS As SmsRequest) As TwiMLResult
Dim SMSResponse As New MessagingResponse
Dim SMSMessage As String = IncomingSMS.Body
Dim JediCode As String = "There is no emotion, there is peace."
Dim SithCode As String = "Peace is a lie. There is only Passion."
JediCode += vbCrLf & "There is no ignorance, there is knowledge."
JediCode += vbCrLf & "There is no passion, there is serenity."
JediCode += vbCrLf & "There is no chaos, there is harmony."
JediCode += vbCrLf & "There is no death, there is the Force."
SithCode += vbCrLf & "Through Passion, I gain Strength."
SithCode += vbCrLf & "Through Strength, I gain Power."
SithCode += vbCrLf & "Through Power, I gain Victory."
SithCode += vbCrLf & "Through Victory my chains are Broken."
SithCode += vbCrLf & "The Force shall free me."
If SMSMessage IsNot Nothing Then
If SMSMessage.ToUpper.Trim = "JEDI" Then
SMSResponse.Message(JediCode)
ElseIf SMSMessage.ToUpper.Trim = "SITH" Then
SMSResponse.Message(SithCode)
Else
SMSResponse.Message("Ahsoka? Is that you?")
End If
Else
SMSResponse.Message("What did you want to know?")
End If
Return TwiML(SMSResponse)
End Function
End Class
End Namespace
Yes, this is all just "play" stuff that I'm using for testing and will eventually be replaced with something more appropriate to the purpose, but I want to try and figure it all out before I get too deep into the reality of things.
I've set up the site on my IIS server, registered the DNS, and even gotten my SSL certificate set up. Everything seems to be working great with my simple testing so far, but there are a couple of things that I still haven't been able to figure out so far and I'm hoping someone here can point me in the right direction.
I'll ask each as a separate question, but here's the first: how do I retrieve the attachment from an MMS message?
I'd like to be able to receive PDFs (and possibly other file types) and pass them along via email to an appropriate individual or department. I know how to do the emailing, but I haven't been able to find appropriate documentation for how to retrieve the attachment(s) in the MMS message to actually include it in that email process.
When I try to access the properties of the IncomingSMS (SmsRequest) object, I don't find any reference to Media in any of them - no NumMedia, no MediaUri, nothing. There doesn't appear to be an MmsRequest object type (that I've found yet, anyway).
What am I overlooking here to be able to retrieve the PDF I sent to my test number for further processing? Should I change the method's definition to accept the object as a MessageResource or something?
EDIT: I forgot to mention that I checked the Twilio console and see that the message was apparently received successfully with the attachment, so I know that at least that part is working properly.
I've asked a second, related question that goes along with this one to help "finalize" some things for our goals.
The SmsRequest class that you're using comes from the Twilio helper library for ASP.NET which aims to make it easier to integrate Twilio into ASP.NET. However, the SmsRequest and other classes do not cover all possible webhook parameters. If there's parameters missing from the class, you can still retrieve the parameters manually instead of relying on MVC Model Binding.
Based on this C# sample, I created a VB.NET sample to show how to receive and save incoming MMS files:
Imports System.IO
Imports System.Net.Http
Imports System.Threading.Tasks
Imports MimeTypes
Imports Twilio.AspNet.Mvc
Imports Twilio.TwiML
Imports Twilio.TwiML.Messaging
Public Class HomeController
Inherits TwilioController
Shared httpClient As HttpClient = New HttpClient
Async Function Index() As Task(Of TwiMLResult)
Dim response As New MessagingResponse
Dim message As New Message
Dim numMedia = Short.Parse(If(Request.Form.Get("NumMedia"), 0))
If numMedia = 0 Then
response.Message("No file received.")
Return TwiML(response)
End If
For mediaIndex As Integer = 0 To numMedia - 1
Dim mediaUrl = Request.Form.Get($"MediaUrl{mediaIndex}")
Dim contentType = Request.Form.Get($"MediaContentType{mediaIndex}")
Dim saveFilePath = Server.MapPath(String.Format(
"~/App_Data/{0}{1}",
Path.GetFileName(mediaUrl),
MimeTypeMap.GetExtension(ContentType)
))
Await DownloadUrlToFileAsync(mediaUrl, saveFilePath)
Next
response.Message("File received.")
Return TwiML(response)
End Function
Private Async Function DownloadUrlToFileAsync(mediaUrl As String, saveFilePath As String) As Task
Dim Response = Await httpClient.GetAsync(mediaUrl)
Dim httpStream = Await Response.Content.ReadAsStreamAsync()
Using fileStream As Stream = IO.File.Create(saveFilePath)
Await httpStream.CopyToAsync(fileStream)
Await fileStream.FlushAsync()
End Using
End Function
End Class
You can probably simplify the code a little by assuming there's only one file going to be sent over MMS, but it's a good idea to handle other cases too.
To get the correct file extension, I'm using this
MimeTypeMap library, but you can roll your own solution.
By default the files from the incoming MMS are publicly available via the MediaUrl{mediaIndex} URL, but it's a good idea to turn on Basic Auth for these media files.
If you're turning on Basic Auth for media files, you'll need to add the authentication header to the HTTP requests, like this:
Imports System.IO
Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Threading.Tasks
Imports MimeTypes
Imports Twilio.AspNet.Mvc
Imports Twilio.TwiML
Imports Twilio.TwiML.Messaging
Public Class HomeController
Inherits TwilioController
Shared httpClient As HttpClient = CreateHttpClient()
Private Shared Function CreateHttpClient() As HttpClient
Dim client As New HttpClient
Dim appSettings As NameValueCollection = ConfigurationManager.AppSettings
If Boolean.Parse(If(appSettings.Get("TwilioUseBasicAuthForMedia"), False)) Then
Dim authString = $"{appSettings.Get("TwilioAccountSid")}:{appSettings.Get("TwilioAuthToken")}"
authString = Convert.ToBase64String(Encoding.ASCII.GetBytes(authString))
client.DefaultRequestHeaders.Authorization = New AuthenticationHeaderValue("Basic", authString)
End If
Return client
End Function
...
End Class
I'm retrieving the TwilioUseBasicAuthForMedia, TwilioAccountSid, and TwilioAuthToken from the Web.config appSettings.
(Make sure you don't check those secrets into source control, and use UserSecretsConfigBuilder instead, to securely set the secrets).
Here's the source code on GitHub.
You're welcome to submit a GitHub issue and/or a PR to add support for these missing parameters to the SmsRequest. Tho, it wouldn't be as easy as normal model binding because the number of parameters increases as more files are sent over MMS.
Related
TL;DR:
what's the MS Graph API for setting the teams call forwarding policy of an user?
Background:
Currently I'm trying to migrate a Lync/Skype-based .NET application to Teams.
The Teams related part is about setting the call forwarding preferences of a few specific users.
Those users have direct routing enabled, i.e. you can call a fixed PSTN/suffix number and the user will receive the call on his mobile. That mobile number is depending on the shift, so the programm is adapting it to whoever has the shift duty at that time.
What I've tried so far?
I can authenticate with the MS Graph API.
I know that there's TAC extension for this purpose ([1] and [2])
There's also a Powershell extension [3]
I'm not the first one to ask the question, but other threads usually got stuck [4]
The Call-Redirect is not what I want, as I'm not actively listening on those instances.
There's a github for Teams related scripts, but unfortunately without sources ...
I haven't yet reflected the Powershell extension
There is a promising user settings entry, where you can change shifts but not the call forwarding
Plan B?
invoke the powershell cmdlet, but that seems to be so 2000-ish
Update 2022-06-20
I'm starting to reflect the ps module. So API seems to be something like https://api.interfaces.records.teams.microsoft.com/Skype.VoiceGroup/userRoutingSettings/ + userId
The teams user id can be retrieved
Some parts of teams rely still on an older REST API (german only, sorry)
Update 2022-06-30
a POC which can be improved would look like this (... if I've packed into the usual AcquireTokenOnBehalfOf, then I'll add it as an answer ...)
Imports System.IO
Imports Microsoft.Identity.Client
Imports System.Globalization
Imports System.Net.Http
Imports System.Text
Imports System.Net.Http.Headers
Imports System.Net
Imports Newtonsoft.Json
Imports System.IdentityModel.Tokens.Jwt
Public Class LyncTest
Public Shared Sub Test()
Dim InstanceId As String = "https://login.microsoftonline.com/"
Dim RedirectURI As String = "https://login.microsoftonline.com/common/oauth2/nativeclient"
' Ids / Secrets and can be found on your Azure application page
Dim TenantId As String = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Dim AppId As String = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Dim secretVal As String = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Dim username As String = "xxxxxxx.xxxxxx#xxxxxxxxxxxx.com"
Dim password As String = "xxxxxxxxxxxx"
' Teams scope
Dim scope As String = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"
Dim httpClient = New HttpClient()
' start resource owner password credential flow
' see https://learn.microsoft.com/en-us/powershell/module/teams/connect-microsoftteams?view=teams-ps#example-4-connect-to-microsoftteams-using-accesstokens
Dim baseParam As String =
$"client_id={AppId}" &
$"&username={WebUtility.UrlEncode(username)}" &
$"&password={WebUtility.UrlEncode(password)}" &
$"&grant_type=password" &
$"&client_secret={WebUtility.UrlEncode(secretVal)}" &
$"&scope={scope}"
' get user_impersonation token
Dim tokenReq As New HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token") With {
.Content = New StringContent(baseParam, Encoding.UTF8, "application/x-www-form-urlencoded")
}
Dim TokenRes As HttpResponseMessage = httpClient.SendAsync(tokenReq).Result
Dim TokenObj As GraphToken = JsonConvert.DeserializeObject(Of GraphToken)(TokenRes.Content.ReadAsStringAsync.Result())
Dim JwtReader As New JwtSecurityTokenHandler
Dim JwtToken As JwtSecurityToken = JwtReader.ReadToken(TokenObj.AccessToken)
Dim UserOid As String = JwtToken.Payload("oid")
' set user calling routing
Dim RoutingURL As String = $"https://api.interfaces.records.teams.microsoft.com/Skype.VoiceGroup/userRoutingSettings/{UserOid}"
httpClient.DefaultRequestHeaders.Authorization = New AuthenticationHeaderValue("Bearer", TokenObj.AccessToken)
Dim RoutingJSON As String =
"{""sipUri"":""sip:andreas.beeker#kraiburg-tpe.com""," &
"""forwardingSettings"":{""isEnabled"":false,""forwardingType"":""Simultaneous"",""targetType"":""Unknown"",""target"":""""}," &
"""unansweredSettings"":{""isEnabled"":true,""targetType"":""SingleTarget"",""target"":""+491701234567"",""delay"":""00:00:20""}," &
"""callGroupDetails"":{""targets"":[],""order"":""Simultaneous""},""callGroupMembershipSettings"":{""callGroupMembershipDetails"":[]}}"
Dim RoutingReq As New HttpRequestMessage(HttpMethod.Post, RoutingURL) With {
.Content = New StringContent(RoutingJSON, Encoding.UTF8, "application/json")
}
Dim RoutingRes As HttpResponseMessage = httpClient.SendAsync(RoutingReq).Result
Console.WriteLine(If(RoutingRes.IsSuccessStatusCode, "success", "failed"))
End Sub
Public Class GraphToken
<JsonProperty(PropertyName:="access_token")>
Public Property AccessToken As String
<JsonProperty(PropertyName:="expires_in")>
Public Property ExpiresIn As Integer
<JsonProperty(PropertyName:="ext_expires_in")>
Public Property ExpiresInExt As Integer
<JsonProperty(PropertyName:="scope")>
Public Property Scope As String
<JsonProperty(PropertyName:="token_type")>
Public Property TokenType As String
End Class
End Class
There is no Graph API available for setting the teams call forwarding policy of an user.
Any user policy/tenant configuration are only exposed through PowerShell or admin portal now.
If you just want to change the forwarding status in Teams without catching callId and transferring individual calls, there is a simpler way by using PowerShell:
Install PowerShell module for M$Teams
https://www.powershellgallery.com/packages/MicrosoftTeams straight to PowerShell, for example by using
Install-Module -Name MicrosoftTeams -RequiredVersion 4.6.0 -AllowClobber
Assuming that you have permission set up forwarding by using Set-CsUserCallingSettings command, for example
Set-CsUserCallingSettings -Identity user#email.com -IsUnansweredEnabled $FALSE
Set-CsUserCallingSettings -Identity user#email.com -IsForwardingEnabled $true -ForwardingType Immediate -ForwardingTargetType SingleTarget -ForwardingTarget "+123456789"
In theory, only the second line is necessary, but I've noticed that PowerShell throws an error if Voicemail is enabled
This will change the forwarding status for all calls incoming to the selected identity. Note, that the user can change it still in Teams GUI.
I am trying to add a grpc protofile to my swagger-ui. I am consuming a grpc webservice which needs a protofile as input. The input to my spring boot restful webservice needs to have that same grpc structure as its interface. I recevied a jar from the individual that made the protofile and imported it to my webserivce. When I try to add the #ResponseBody tag around the object from the protofile jar, my app hangs on this in the console at startup:
s.d.s.w.s.ApiListingReferenceScanner : Scanning for api listing references
Thanks,
Brian
Never return entity objects in controller method.
in my case. my Controller methods takes this parameter.
"#AuthenticationPrincipal UserSession userSession"
when i exlude UserSession object swagger back to normal.
There were 2 way to do that
first is "#ApiIgnore #AuthenticationPrincipal UserSession userSession"
second is in swaggerConfig class
private Class[] clazz = {UserSession.class};
Docket().ignoredParameterTypes(clazz)
Incase someone needs a solution, what I did was as a work around for now.
in my service's code (response is a String)
return JsonFormat.printer().print(myProtoObject);
in my client's code:
Builder b = ProtoObject.newBuilder();
JsonFormat.parser().merge(result.getBody(), b);
ProtoObject protoObject = b.build();
Using DisplayModeProvider to choose between views for "Desktop", "Tablet" and "Phone" in a MVC5 web application. It is my understanding that this class selects the correct provider in order and uses the first provider that returns True. However, when I step through the code, I find there is a repeated cycle through the code (it goes through multiple times, sometimes over 10 cycles) before deciding on the proper mode. I'm using WURFL Cloud for device detection. Lastly, I've started caching WURFL results in a Session variable. Thinking there must be something wrong with my code and/or logic. It's in VB.net since it's an evolution of a legacy project. The first block of code is in Application_Start in global.asax. Before it was in a separate class, but moved it to global.asax in an attempt to solve this problem.
DisplayModeProvider.Instance.Modes.Clear()
DisplayModeProvider.Instance.Modes.Add(New DefaultDisplayMode("Phone") With {.ContextCondition = Function(c) c.Request.IsPhone})
DisplayModeProvider.Instance.Modes.Add(New DefaultDisplayMode("Tablet") With {.ContextCondition = Function(c) c.Request.IsTablet})
DisplayModeProvider.Instance.Modes.Add(New DefaultDisplayMode("") With {.ContextCondition = Function(c) c.Request.IsDesktop})
My understanding is the function would check for each context condition and stop at the first one that is true. However, as mentioned above the code repeatedly executes even though one of the functions returns True.Here are the extension methods I'm using. They reside in a module. The error handling code was added after a "perceived" outage of the WURFL cloud. Each is decorated with the following: System.Runtime.CompilerServices.Extension
Public Function IsPhone(request As HttpRequestBase) As Boolean
Dim ans As Boolean
Try
If Not HttpContext.Current.Session("IsPhone") Is Nothing Then
ans = HttpContext.Current.Session("IsPhone")
Else
wsm = New WURFLServiceModel(New HttpContextWrapper(HttpContext.Current))
ans = wsm.IsPhone
HttpContext.Current.Session("IsPhone") = ans
End If
Catch ex As Exception
...
End Try
Return ans
End Function
Public Function IsTablet(request As HttpRequestBase) As Boolean
Dim ans As Boolean
Try
If Not HttpContext.Current.Session("IsTablet") Is Nothing Then
ans = HttpContext.Current.Session("IsTablet")
Else
wsm = New WURFLServiceModel(New HttpContextWrapper(HttpContext.Current))
ans = wsm.IsTablet
HttpContext.Current.Session("IsTablet") = ans
End If
Catch ex As Exception
...
End Try
Return ans
End Function
Public Function IsDesktop(request As HttpRequestBase) As Boolean
Return True
End Function
Here is the code for the WURFLServiceModel
Imports ScientiaMobile.WurflCloud.Device
Public Class WURFLServiceModel
Private mIsIOS As Boolean
Private mIsTablet As Boolean
Private mIsPhone As Boolean
Private mResponse As String
Private mErrors As Dictionary(Of String, String)
Private api_Key As String = "xxxxxxxxxxxxxxxxxxxxxxxxxx"
Public Sub New(ByVal request As HttpContextBase)
GetDataByRequest(request)
End Sub
Public Sub GetDataByRequest(context As HttpContextBase)
Dim config = New DefaultCloudClientConfig(api_Key)
Dim manager = New CloudClientManager(config)
Dim info = manager.GetDeviceInfo(context)
mIsIOS = info.Capabilities("is_ios")
mIsPhone = info.Capabilities("is_smartphone")
mIsTablet = info.Capabilities("is_tablet")
mBrandName = info.Capabilities("brand_name")
mModelName = info.Capabilities("model_name")
mErrors = info.Errors
mResponse = info.ResponseOrigin
End Sub
Public ReadOnly Property IsDesktop As Boolean
Get
Return True
End Get
End Property
Public ReadOnly Property IsIOS As Boolean
Get
Return mIsIOS
End Get
End Property
Public ReadOnly Property IsTablet As Boolean
Get
Return mIsTablet
End Get
End Property
Public ReadOnly Property IsPhone As Boolean
Get
Return mIsPhone
End Get
End Property
Although the application runs without error, I can't believe the cycling through this routine should be happening. Would like to clear it up, if possible. What am I doing wrong? Many thanks in advance!
As I see it, the issue has more to do with the internal implementation of MVC display modes than the WURFL API. The code bound to the display mode delegate is called back by ASP.NET MVC for each request to render a view, including partial views. This obviously results in multiple calls being made to the WURFL API. In addition, the WURFL Cloud API takes a while to respond because it has to make a HTTP request to the cloud, parse a cookie, and figure out details. The WURFL Cloud is clearly slower than the on-premise WURFL API which uses a direct access memory cache to pick up details of the user agent. I have used WURFL and MVC in a number of web sites and just went through this. For most of such sites I managed to get an on-premise license. As for the cloud, some per-request internal caching perhaps within your WURFLServiceModel class would be helpful so that you end up making a single cloud request for each rendering of the view. I don't particularly like the use of Session, but yes that could just be me. Session is still an acceptable way of do the "internal caching" I was suggesting above.
Luca Passani, ScientiaMobile CTO here. I instructed the support and engineering teams to reach out to you offline and work with you to get to the end of the issue you are experiencing. For the SO admins. We will report a summary here when the issue is identified and resolved. Thanks.
I am developing an integration with Google Calender from a vb.net web application. So far I understand that I need my own implementation of iDataStore as I will be storing refresh tokens belonging to different users on my website with the view to connect to their calendar later and add events.
Here's my code for creating a new authorised user:
Dim clientSecrets As New ClientSecrets
clientSecrets.ClientId = "MyClientID"
clientSecrets.ClientSecret = "MyClientSecret"
Dim input() As String = {"https://www.googleapis.com/auth/calendar"}
Dim scope As New List(Of String)(input)
Dim myStoredResponse As New GoogleOauthAPI.StoredResponse(txtrefreshToken.Text)
Dim savedDataStoreObj As New GoogleOauthAPI.SavedDataStore(myStoredResponse)
Dim credential As UserCredential = GoogleWebAuthorizationBroker.AuthorizeAsync(clientSecrets, scope, "user", Threading.CancellationToken.None, savedDataStoreObj).Result
I am getting the following error when attempting to create the UserCredential object:
Value cannot be null. Parameter name: task
The GoogleOauthAPI.StoredResponse was based on the following guide but converted from C# to vb.net. I had some uncertainty as to whether the conversion of the code was done correctly.
Here is the original C# class
and here is the class converted to vb
When the myStoredResponse object is created I can inspect it in debug mode and it looks like it's pulling in the refresh code, I'm just uncertain why the user credentials cannot be created and throws this error?
Any help would be much appreciated!
Solved the problem in the end. When converting the iDataStore interface class from C# to VB.net I did not have the Implements IDataStore.MethodName appended to each function.
Here is my revised iDataStore VB class, feel free to use this in your own projects.
Ok i have a fairly simple request, but i have been searching for some hours now and can't seem to find a simple answer to what i feel should be a fairly simple task.
I have built an intranet site for our business using dot net & MVC this runs on a separate server to the sharepoint site. All i want to do is get a list of documents from a given sharepoint library and display links to their urls which will redirect the end user to that document from my intranet site - this will vary depending on the users dept.
So i was looking at the jquery AJAX route, but then i realise sharepoint wont allow me javascript queries from a different domain.
So next route is backend dot net pass the data to the view and display that way, but i want to use the servers sharepoint authentication without entering username and password in the code/manually.
Am i missing something simple? I don't want to waste a lot of time on something i thought should be fairly simple.. anyone fancy pointing me in the right direction?
Thanks
Well it wasn't incredibly easy but i did get it done like this if anyone wants to know :p sp 2013 using Rest api using server credentials to connect to sharepoint and retrieve documents Name, Sharepoint url and icon.
Imports System.Xml
Imports System.IO
Imports System.DirectoryServices.AccountManagement
Imports System.Linq
Imports System.Net
Public Class SPDocumentLibrary
Public Property Files As List(Of SPDocument)
Public Sub New()
Dim request As HttpWebRequest = DirectCast(WebRequest.Create(
"http://sharepointserver/_api/web/lists/getByTitle('list')/items?$expand=File&$select=File/Name,OData__dlc_DocIdUrl/Url&$orderby=Title&$top=99999&orderby=File/Name"
), HttpWebRequest)
request.Method = "GET"
request.ContentType = "text/xml"
request.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials
Dim response As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
Dim receiveStream As Stream = response.GetResponseStream()
Dim readStream As New StreamReader(receiveStream, Encoding.UTF8)
Dim xd As XDocument = XDocument.Load(readStream)
response.Close()
readStream.Close()
Dim d As XNamespace = "http://schemas.microsoft.com/ado/2007/08/dataservices"
Dim x As XNamespace = "http://www.w3.org/2005/Atom"
Dim docs As IEnumerable(Of SPDocument) = _
From doc As XElement In xd.Root.Elements(x + "entry") _
Select New SPDocument With {.Name = Path.GetFileNameWithoutExtension(doc.Descendants(d + "Name").Value), .Url = doc.Descendants(d + "Url").Value, .icon = IconFromExtension(Path.GetExtension(doc.Descendants(d + "Name").Value).ToLower)}
Me.Files = docs.ToList()
End Sub
Private Function IconFromExtension(ext As String) As String
If ext Like ".doc*" Then
Return "icdoc.png"
ElseIf ext Like ".xls*" Then
Return "icxls.png"
ElseIf ext Like ".ppt*" Then
Return "icppt.png"
ElseIf ext = ".pdf" Then
Return "icpdf.png"
ElseIf ext = ".msg" Then
Return "icmsg.png"
ElseIf ext Like ".htm*" Then
Return "ichtm.gif"
ElseIf ext Like ".rtf" Then
Return "icrtf.gif"
ElseIf ext Like ".dot*" Then
Return "icdot.png"
ElseIf ext Like ".xlt*" Then
Return "icxlt.png"
Else
Return "icgen.gif"
End If
End Function
End Class
Public Class SPDocument
Property Name As String
Property Url As String
Property icon As String
End Class