I am using DNN 9 and I want to send a notification with an attachment file, but seems that DNN doesn't allow to do that.
Is there is a way (or any workaround) to do that?
Here is the DNN code of the NotificationsController
and here is my code that calls the DNN code
//...
Notification dnnNotification = new Notification
{
NotificationTypeID = notification.NotificationTypeId,
From = notification.From,
Subject = notification.Subject,
Body = notification.Body
};
NotificationsController.Instance.SendNotification(dnnNotification, portalId, dnnRoles, dnnUsers);
You cannot attach a file to a notification in DNN. BUT, you can add custom notification actions to a notification type. These actions result in links added under the notification (like the default "Dismiss" action to mark the notification as "read").
In order to send a notification, you need to create a NotificationType to associate that to. The NotificationTypeAction are added to the type. So whenever you send a notification of a certain type, the actions go with it.
You could create a NotificationTypeAction and name it "Download Attachment". When a user clicks the link, it will call a custom api service. That service could serve up the file.
Here is some sample code in which I create a custom type with 1 custom action:
public void AddNotificationType()
{
var actions = new List<NotificationTypeAction>();
var deskModuleId = DesktopModuleController.GetDesktopModuleByFriendlyName(Constants.DESKTOPMODULE_FRIENDLYNAME).DesktopModuleID;
var objNotificationType = new NotificationType
{
Name = Constants.NOTIFICATION_FILEDOWNLOAD,
Description = "Get File Attachment",
DesktopModuleId = deskModuleId
};
if (NotificationsController.Instance.GetNotificationType(objNotificationType.Name) == null)
{
var objAction = new NotificationTypeAction
{
NameResourceKey = "DownloadAttachment",
DescriptionResourceKey = "DownloadAttachment_Desc",
APICall = "DesktopModules/MyCustomModule/API/mynotification/downloadfile",
Order = 1
};
actions.Add(objAction);
NotificationsController.Instance.CreateNotificationType(objNotificationType);
NotificationsController.Instance.SetNotificationTypeActions(actions, objNotificationType.NotificationTypeId);
}
}
Then use code like the following to send the notification:
public void SendNotification(UserInfo userToReceive)
{
// Get the notification type; if it doesn't exist, create it
ModuleController mCtrl = new ModuleController();
var itemAddedNType = NotificationsController.Instance.GetNotificationType(Constants.NOTIFICATION_FILEDOWNLOAD);
if (itemAddedNType == null)
{
AddNotificationType();
itemAddedNType = NotificationsController.Instance.GetNotificationType(Constants.NOTIFICATION_FILEDOWNLOAD);
}
if (itemAddedNType != null)
{
Notification msg = new Notification
{
NotificationTypeID = itemAddedNType.NotificationTypeId,
Subject = "A file is ready to download.",
Body = alertBody,
ExpirationDate = DateTime.MaxValue,
IncludeDismissAction = true,
};
List<UserInfo> sendUsers = new List<UserInfo>();
sendUsers.Add(userToReceive);
NotificationsController.Instance.SendNotification(msg, itemModule.PortalID, null, sendUsers);
}
}
For a full tutorial on DNN Notifications, I highly recommend subscribing to DNNHero.com and watching this 3-part series which comes with sample code.
https://www.dnnhero.com/Premium/Tutorial/ArticleID/265/DNN-Notifications-Introduction-Part-1-3
Related
I'm creating a new customer and adding them to a subscription in one call like so:
StripeConfiguration.SetApiKey(StripeData.ApiKey);
var customerService = new CustomerService();
var myCustomer = new CustomerCreateOptions
{
Email = stripeEmail,
Source = stripeToken,
Plan = StripeData.MonthlySubscriptionPlanId
};
Customer stripeCustomer = customerService.Create(myCustomer);
Then I used to be able to do this:
myLocalUser.StripeCustomerId = stripeCustomer.Id;
myLocalUser.StripeSubscriptionId = stripeCustomer.Subscriptions.Data[0]?.Id;
But now the API isn't returning the customer's subscriptions so the second line fails
I'm now having to call the API again with this ugly code to get the customer's subscriptionId:
if (stripeCustomer.Subscriptions != null)
{
user.StripeSubscriptionId = stripeCustomer.Subscriptions.Data[0]?.Id;
}
else
{
//get subscriptionId
var cust = customerService.Get(stripeCustomer.Id, new CustomerGetOptions
{
Expand = new System.Collections.Generic.List<string> { "subscriptions" }
});
if (cust.Subscriptions.Any())
{
stripeSubscriptionId = cust.Subscriptions.First().Id;
}
}
CustomerService.Create() doesn't have the same Expand parameter option that the Get() method does...
This is expected, as subscriptions are no longer included by default on a customer object unless you expand them since API version 2020-08-27.
Creating a customer with a source and plan is still possible (although not the recommended integration path anymore since you might run into problems with 3DS and tax rates), although since you are on a newer API version you won't get the subscriptions list back. If you can you should update to creating subscriptions via their own API.
If you however still want to use this old integration path, you can still get the subscriptions back in the customer create call, you just need to expand the subscriptions on creation:
var customerService = new CustomerService();
var myCustomer = new CustomerCreateOptions
{
Email = stripeEmail,
Source = stripeToken,
Plan = StripeData.MonthlySubscriptionPlanId
};
myCustomer.AddExpand("subscriptions");
Customer stripeCustomer = customerService.Create(myCustomer);
I'll send an adaptive card to the user with 2 actions, but when I select an action, a null string will be send to the server. Because of that the bot can't understand what the user wants.
As an extra check, I echo everything that the user sends to the bot. There is a null or empty check.
Here you've the conversation in Teams:
Here is the conversation in the emulator:
Here is my code to create the activity:
Activity activity = new Activity()
{
Attachments = new List<Attachment>()
{
new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = new AdaptiveCard("1.0")
{
Body = new List<AdaptiveElement>()
{
new AdaptiveTextBlock()
{
Text = $"Lorem reminder",
Size = AdaptiveTextSize.Large
},
new AdaptiveTextBlock()
{
Text = $"Hi {userName},\r\nYou've missed some lorem ipsum dolor sit amet:\r\n- **consectetur:** Ut porttitor\r\nChoose an action from below to add them."
}
},
Actions = new List<AdaptiveAction>()
{
CreateCardAction("First action"),
CreateCardAction("Second action")
}
}
}
},
From = turnContext.Activity.Recipient,
Recipient = turnContext.Activity.From,
Type = "message",
Conversation = turnContext.Activity.Conversation
};
private AdaptiveAction CreateCardAction(string text)
{
return new AdaptiveSubmitAction()
{
Title = text,
Data = text
};
}
Using type imBack for the AdaptiveSubmitAction (like the suggested actions) don't work here. See screenshot from the emulator below.
If you run this locally, using NGrok, it's worth having a look at the payload in the ngrok inspector so that you see what's send back (http://localhost:4040/).
Essentially, "Data" is just an "object" type so that you can submit whatever you want on there. Let's say, for instance, that you want it to send back an "id" value - simple attach that as the "Data" property, like:
private AdaptiveAction CreateCardAction(string text, string id)
{
return new AdaptiveSubmitAction()
{
Title = text,
Data = id
};
}
Alternatively, it can be a more complex object, like:
public class Foo {
public string something { get; set; }
public string id{ get; set; }
}
private AdaptiveAction CreateCardAction(string text, Foo foo)
{
return new AdaptiveSubmitAction()
{
Title = text,
Data = foo
};
}
then when the message comes in, on your OnTurnAsync, you can check if the Turncontext.Activity.Value contains something.
Actually, it seems that Teams strips away plain string "Data" property (which works perfectly fine on DirectLine channel instead).
If you are able to replace the plain string by a JObject, the problem is solved. If - for any reason - you want your bot to act as a "bridge" and forward to Teams channel an AdaptiveCard with a plain string "Data" property coming from a different channel (i.e. DirectLine), than wrapping the "Data" string content in a JObject could save your day.
I'm using Microsoft.Graph to create an Office365 calendar event. It works fine and I can create an event but I need to add a couple of extra string properties to an event, so I've created an extension for that. It compiles fine. But when I try to run it and create an event with added extension, it throws an error:
Code: RequestBodyRead
Message: The property 'extensionName' does not exist on type 'Microsoft.OutlookServices.Extension'. Make sure to only use property names that are defined by the type or mark the type as open type.
//Extension
var evExtCollPage = new EventExtensionsCollectionPage();
var dict = new Dictionary<string,object>();
dict.Add("eSmtTickeId", "123");
dict.Add("siteId", "456");
var openExtension = new OpenTypeExtension
{
ExtensionName = "com.TechApp.Extensions",
AdditionalData = dict
};
evExtCollPage.Add(openExtension);
Event createdEvent = await graphClient.Me.Events.Request().AddAsync(new Event
{
Subject = "Service appointment",
Location = location,
Attendees = attendees,
Body = body,
Start = startTime,
End = endTime,
Extensions = evExtCollPage
});
What is wrong with my extension? I've struggled with this for 3 days now.
Adding the ODataType has worked for me:
var openExtension = new OpenTypeExtension
{
ODataType = "microsoft.graph.openTypeExtension",
ExtensionName = "com.TechApp.Extensions",
AdditionalData = dict
};
I have a C# managed Application that runs on a Lync 2013 Server and uses MSPL. I route every call from MSPL to the application and handle it there. Lync to Lync calls work fine and their to Header is in the form sip:user#domain.com. But when a call from outside the network (non-lync like mobile phone etc.) to the workphone of a Lyncuser is started, the Uri is like sip:+12341234#domain.com;user=phone (sip:[workphone]#domain). Passing this string to the Presence Retrieval function does not work.
var sips = new string[] { phone }; // The "To" number
presenceService.BeginPresenceQuery(sips, categories, null, null, null);
This always returns an empty result. How can I first retrieve the user associated with the phone number to get its presence?
I solved it this way:
public static UserObject FindContactBySip(string sip)
{
return UserList.FirstOrDefault(u => u.HasSip(sip));
}
private static void InitFindUsersInAD()
{
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
var user = new UserPrincipal(ctx);
user.Name = "*";
var searcher = new PrincipalSearcher(user);
var result = searcher.FindAll();
var sipList = new List<string>();
UserList = new List<UserObject>();
foreach (var res in result)
{
var underlying = (DirectoryEntry)res.GetUnderlyingObject();
string email = string.Empty, phone = string.Empty, policies = string.Empty;
foreach (var keyval in underlying.Properties.Values)
{
var kv = keyval as System.DirectoryServices.PropertyValueCollection;
if (kv != null && kv.Value is string)
{
if (kv.PropertyName.Equals("msRTCSIP-PrimaryUserAddress"))
{
email = (kv.Value ?? string.Empty).ToString();
}
else if (kv.PropertyName.Equals("msRTCSIP-Line"))
{
phone = (kv.Value ?? string.Empty).ToString();
}
else if (kv.PropertyName.Equals("msRTCSIP-UserPolicies"))
{
policies = (kv.Value ?? string.Empty).ToString();
}
}
}
if (!string.IsNullOrEmpty(phone) && !string.IsNullOrEmpty(email))
{
var userobj = new UserObject(email, phone, policies);
UserList.Add(userobj);
}
}
}
First I initialize the UserList (List // Custom class) from the AD. Then I call FindContactBySip and check if the provided SIP equals the Email or Phone of the User.
I have found two other ways to solve your problem.
In MSPL you can:
toContactCardInfo = QueryCategory(toUserUri, 0, "contactCard", 0);
Which gives you:
<contactCard xmlns=""http://schemas.microsoft.com/2006/09/sip/contactcard"" >
<identity >
<name >
<displayName >
Lync User</displayName>
</name>
<email >
lync.user#xxx.com</email>
</identity>
</contactCard>
You can turn the email address into a sip address. This only works if your lync setup uses email address for sip addresses.
The other method is to use 'P-Asserted-Identity' sip header to determine who the phone call is being routed to/from. The only problem is that it doesn't show up in the inital invites (as that would be for the From side anyway), but in the 180 ringing response from the Lync Client.
P-Asserted-Identity: <sip:lync.user#xxx.com>, <tel:+123456789;ext=12345>
So if you wait for the 180 ringing response then I would recommand that you use P-Asserted-Identity method and you don't even need to escape out of MSPL for it!
I am trying to delete message from inbox. Message doesn't have messageId(it's not gmail)
I've found in this topic the solution
MailSystem.Net Delete Message, IndexOnServer Property = 0
However it doesn't work for me.
The code below is simplifed the application works as MVVM light application.
This is how I get emails.
for (int i = 1; i <= mails.MessageCount; ++i)
{
Message msg = fetch.MessageObject(i);
MessageModel tmpMsg = new MessageModel() { messageType = MessageModel.MessageType.INBOX, UID = i, Date = msg.Date, Body = content, Title = msg.Subject, Sender = msg.From.Merged, Receiver = msg.To[0].Merged, IsEncrypted = isEncrypted };
tmpMsg.MessageWasDeleted += tmpMsg_MessageWasDeleted;
InboxMessages.Messages.Add(tmpMsg); //this is observable collection of MessageModel
}
I've tested this code and proper id is added to messageModel object.
Now when I want to delete the message I use this code in model:
var imapClient = new Imap4Client();
imapClient.BeginConnect(ConfigurationManager.AppSettings["mail_smtp_host"], ConfigurationManager.AppSettings["mail_smtp_login"], ConfigurationManager.AppSettings["mail_smtp_password"], new AsyncCallback((res) =>
{
imapClient.EndConnect(res);
//var box = imapClient.AllMailboxes["INBOX"] ;
var box = imapClient.SelectMailbox("INBOX");
Message email = box.Fetch.MessageObject(UID); //this is just to check if proper message is going to be deleted
box.UidDeleteMessage(UID, true);
if (MessageWasDeleted != null) MessageWasDeleted(this, new EventArgs());
}));
This method removes the message based on UID(at least it should do it) but it doesn't. There is no error nothing appears.
Where I could make an error? What's the proper way to delete message using ActiveUpMail?