Skip to content

Commit eaa0b00

Browse files
committed
Version 1.1.0
1 parent a6b576b commit eaa0b00

152 files changed

Lines changed: 10284 additions & 454 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,67 +6,122 @@ This module has a single purpose: send messages to Slack channels.
66

77
Its features are
88

9-
* uses Slack incoming Webhooks
10-
* send messages to multiple channels in multiple workspaces
9+
* uses Slack Incoming Webhooks
10+
* Incoming Webhook URLs are stored encrypted
1111
* create messages with markdown markup or just plain text
12-
* specify message to be shown in the notification
13-
* no Java, just native Mendix
12+
* send messages to multiple channels in multiple workspaces
13+
* message can be sent in the background not delaying your primary process
14+
* no Java libraries that requires maintenance, just native Mendix
15+
* Dutch and English languages are supported in the UI
1416

1517
## Installation
1618

1719
Download the module from the AppStore and add it to your project.
1820

21+
Add microflow `ASu_SlackMessage` to your app's AfterStartup microflow.
22+
23+
The module depends on AppStore module [CommunityCommons](https://appstore.home.mendix.com/link/app/170/).
24+
25+
## Constants
26+
27+
The module adds these constants to your app and adjust them when needed.
28+
29+
* `SlackMessage.AutoCreateIncomingWebhook` - Automatically create the requested webhook, marked invalid, to be completed later. Every used webhook should be defined and marked valid. But when an unknown webhook is used then it is added to be completed manually.
30+
* `SlackMessage.SendEnabled` - By default messages are sent on test, acceptance and production systems. In case you want to disable sending messages on a specific system then set this constant to FALSE in the environment's configuration.
31+
* `SlackMessage.SendEnabledInDev` - By default messages are not sent on development systems. This prevents messages being sent while developing an app. However when you want messages to be sent, change this constant value to TRUE in your project's profile.
32+
* `SlackMessage.SlackRequestTimeout` - Slack web service request timeout in seconds.
33+
34+
This constant is added and you have to change it because the default value is unusable:
35+
36+
* `SlackMessage.EncryptionKey` - The encryption key used to encrypt sensitive data like the Incoming Webhook URL which does not need authentication. Its length should exactly be 16 characters (128 bit). By default this key is not set so you have to create your own secure key. It is advised to use a combination of uppercase and lowercase characters, digits and special characters.
37+
38+
This constant is added but you should not change it:
39+
40+
* `SlackMessage.EncryptionPrefix` - A string that indicates if a string is encrypted or not. Changing it might prevent you to upgrade in the future.
41+
1942
## Security
2043

2144
The module offers these two roles:
2245

23-
* **Administrator** - manage Webhooks
24-
* **User** - the minimum needed for regular users
46+
* `Administrator` - manage Webhooks
47+
* `User` - the minimum needed for regular users
48+
49+
## Create Incoming Webhook(s) in Slack
50+
51+
First you have to create one or more Incoming Webhooks in Slack. You [can create an app](https://api.slack.com/messaging/webhooks) that adds the Webhook or add the [Incoming Webhook app](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) to your Slack workspace and configure it. Which method you prefer is up to you, however Slack seems to prefer the first. In the end you need a Webhook URL to use the module. Such URL looks like `https://hooks.slack.com/services/somestring/anotherstring/alongerstring`.
2552

26-
## Create incoming Webhook(s)
53+
## Store Incoming Webhook(s) in your app
2754

28-
First you have to create one or more incoming Webhooks in Slack. You [can create an app](https://api.slack.com/messaging/webhooks) that adds the Webhook or add the [Incoming Webhook app](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) to your Slack workspace and configure it. Which method you prefer is up to you. In the end you need a Webhook URL to use the module. Such URL looks like `https://hooks.slack.com/services/somestring/anotherstring/alongerstring`.
55+
The module offers two methods of storing and using Incoming Webhook URLs:
2956

30-
A Webhook has these properties (all are required):
57+
1. create IncomingWebhook objects (provided by the module) where the URL is stored encrypted and in your microflows you refer to it using a unique key
58+
2. store URLs using your own method and provide the clear text URL when sending messages
3159

32-
* **Label** - a descriptive human readable label
33-
* **Key** - a sort unique key to be used in your microflows to find the right Webhook; only lowercase and uppercase characters, digits and underscores are allowed here
34-
* **URL** - the incoming Webhook URL
60+
Which way is used to store the URLs in an app is not that important, pick what fits your app and requirements best. The convenience microflows described below support both.
3561

36-
Add snippet **SN_Webhook** to a page to manage your Webhooks. Additionally you can use microflow **Webhook_Ensure** in for example the AfterStartup flow to add the Webhooks to the database on app start.
62+
### Store in entity IncomingWebhook
63+
64+
One way is to store URLs in entity `IncomingWebhook`. This allows the developer to store URLs **encrypted** and use multiple Incoming Webhooks in the app easily. The URLs are stored encrypted because they can be used without any means of authentication.
65+
66+
The entity has these attributes (all are required):
67+
68+
* `Label` - a descriptive human readable label
69+
* `Key` - a sort unique key to be used in your microflows to find the right Webhook; only lowercase and uppercase characters, digits and underscores are allowed here
70+
* `URL` - the Incoming Webhook URL
71+
72+
It is highly recommended to store the values for `Key` in an enumeration. This makes using them more consistent and prevents error by typos.
73+
74+
Add snippet `SN_Webhook` to a page to manage your Incoming Webhooks manually. Additionally you can use microflows `IncomingWebhook_Ensure` or `IncomingWebhook_Upsert` in for example the AfterStartup flow to add them to the database on app start automatically.
75+
76+
### Store using your own method
77+
78+
Another way is to store URLs using your own method and not use entity `IncomingWebhook` at all. For example the app uses just a single Incoming Webhook and its URL is stored in a constant. If encrypting the URL is less important then there is no need to create an `IncomingWebhook` object. The module provides microflows that can send messages to Slack based on a clear text URL.
3779

3880
## Create and send a single line message
3981

40-
Microflow **Message_CreateAndSend** can be used to send a single line message to Slack and can be used as an example how to use all steps separately with multi line messages.
82+
The microflows `Message_CreateAndSendToWebhookKey` and `Message_CreateAndSendToWebhookURL` can be used to send a single line message to Slack and can be used as an example how to use all steps separately with multi line messages.
4183

42-
The microflow has these parameters:
84+
The first microflow expects an `IncomingWebhook` object with the specified `key` to be in the database. The second microflow accepts an Incoming Webhook URL as input.
4385

44-
* **NotificationText** - the text to be shown in the OS notification, max 200 characters
45-
* **NotificationTextType** - specify if the text markup is plain text or markdown
46-
* **MessageText** - the text with the "real" message, max 3000 characters; when *empty* then it is skipped and the notification text is used
47-
* **MessageTextType** - specify if the text markup is plain text or markdown
48-
* **WebhookKey** - the key of the Webhook to send the message to
86+
The microflows have these parameters:
4987

50-
More details about, for example, text markup are in the paragraphs below.
88+
* `NotificationText` - the text to be shown in the OS notification, max 200 characters
89+
* `NotificationTextType` - specify if the text markup is plain text or markdown
90+
* `MessageText` - the text with the "real" message, max 3000 characters; when *empty* then it is skipped and the notification text is used
91+
* `MessageTextType` - specify if the text markup is plain text or markdown
92+
* `IncomingWebhookKey` or `IncomingWebhookURL` - the key or full URL of the Incoming Webhook to send the message to
93+
* `SendAsync` - use TRUE (preferred) to send the message in a separate background thread or FALSE to send it in the same thread
94+
95+
More details about text markup are in the paragraphs below.
96+
97+
On failure check the log file for details.
5198

5299
## Create and send a multi-line message
53100

54-
You start a multi-line message by creating the basis of a message using microflow **Message_Initialize**. It requires a string that contains the text to be shown in the notification created by Slack. So this is not the message in the channel, but an OS notification telling that there is a message. [Markdown](https://api.slack.com/reference/surfaces/formatting) can be used in this message.
101+
You start a multi-line message by creating the basis of a message using microflow `Message_Initialize`. It requires a string that contains the text to be shown in the notification created by Slack. So this is not the message in the channel, but an OS notification telling that there is a message. [Markdown](https://api.slack.com/reference/surfaces/formatting) can be used in this message.
55102

56-
Next you add one or more lines to your message using microflow **Message_AddLine**. Also here [Markdown](https://api.slack.com/reference/surfaces/formatting) is supported.
103+
Next you add one or more lines to your message using microflow `Message_AddLine`. Also here [Markdown](https://api.slack.com/reference/surfaces/formatting) is supported.
57104

58-
An example of the text in such a line is below. The resulting message starts with `@here` which notifies all active users. Next it shows a large blue dot (emoji) and a text where the word **UP** is in **bold**.
105+
An example of the text in such a line is below. The resulting message starts with `@here` which notifies all active users. Next it shows a large blue dot (emoji) and a text where the word `UP` is in **bold**.
59106

60107
```auto
61-
<!here> :large_blue_circle: all systems are *UP*
108+
<!here> :green_heart: all systems are *UP*
62109
```
63110

64-
An easy way to find the name of an emoji is to create a message in Slack manually and add the desired emoji. The popup where you search for your emoji shows the string to use.
111+
An easy way to find the name of an emoji is to create a message in Slack manually, add the desired emoji and use the presented string. The popup where you search for your emoji shows the string to use.
65112

66-
When you have created the message you send it using microflow **Message_Send**. It requires the key of the Webhook that you created before.
113+
When you have created the message then you send it using microflow `Message_SendToWebhookKey` or `Message_SendToWebhookURL`, depending which method to store Incoming Webhook URLs you decided to use. They have these parameters:
114+
115+
* `Message` - the `Message` object you created
116+
* `IncomingWebhookKey` or `IncomingWebhookURL` - the key or full URL of the Incoming Webhook to send the message to
117+
* `SendAsync` - use TRUE (preferred) to send the message in a separate background thread or FALSE to send it in the same thread
67118

68119
On failure check the log file for details.
69120

70121
## Test creating and sending messages
71122

72-
The GitHub repo contains a Mendix model that can be used to test the module before adding it to your project.
123+
[The GitHub repo](https://github.com/ppoetsma/SlackMessage) contains a Mendix model with features that can be used to test the module before adding it to your project. Snippet `SN_Webhook` has test features as well.
124+
125+
## Feedback
126+
127+
Please leave your feedback in [the GitHub repo](https://github.com/ppoetsma/SlackMessage/issues).

dist/SlackMessage110.mpk

88.3 KB
Binary file not shown.

src/SlackMessage.mpr

500 KB
Binary file not shown.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package communitycommons;
2+
3+
import communitycommons.proxies.DatePartSelector;
4+
import static communitycommons.proxies.DatePartSelector.day;
5+
import static communitycommons.proxies.DatePartSelector.month;
6+
import static communitycommons.proxies.DatePartSelector.year;
7+
import java.util.Date;
8+
9+
import java.time.LocalDate;
10+
import java.time.Period;
11+
import java.time.ZoneId;
12+
import java.util.Calendar;
13+
14+
public class DateTime {
15+
16+
/**
17+
* @author mwe
18+
* @author res
19+
* @param firstDate The begin of the period
20+
* @param compareDate The end of the period
21+
* @return The period between the firstDate in the system default timezone, and the compareDate in the system
22+
* default timezone as a Java Period
23+
*
24+
* Code is based on http://stackoverflow.com/questions/1116123/how-do-i-calculate-someones-age-in-java
25+
*
26+
* Adjusted to Java 8 APIs (April, 2019)
27+
*/
28+
public static Period periodBetween(Date firstDate, Date compareDate) {
29+
return Period.between(toLocalDate(firstDate), toLocalDate(compareDate));
30+
}
31+
32+
private static LocalDate toLocalDate(Date someDate) {
33+
return someDate.toInstant()
34+
.atZone(ZoneId.systemDefault())
35+
.toLocalDate();
36+
}
37+
38+
public static long dateTimeToInteger(Date date, DatePartSelector selectorObj) {
39+
Calendar newDate = Calendar.getInstance();
40+
newDate.setTime(date);
41+
int value = -1;
42+
switch (selectorObj) {
43+
case year:
44+
value = newDate.get(Calendar.YEAR);
45+
break;
46+
case month:
47+
value = newDate.get(Calendar.MONTH) + 1;
48+
break; // Return starts at 0
49+
case day:
50+
value = newDate.get(Calendar.DAY_OF_MONTH);
51+
break;
52+
default:
53+
break;
54+
}
55+
return value;
56+
}
57+
58+
public static long dateTimeToLong(Date date) {
59+
return date.getTime();
60+
}
61+
62+
public static Date longToDateTime(Long value) {
63+
return new Date(value);
64+
}
65+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package communitycommons;
2+
3+
import org.apache.commons.lang3.builder.HashCodeBuilder;
4+
5+
6+
public class ImmutablePair<T, U>
7+
{
8+
public static <T, U> ImmutablePair<T, U> of(T left, U right) {
9+
return new ImmutablePair<T, U>(left, right);
10+
}
11+
12+
private final T left;
13+
private final U right;
14+
15+
private ImmutablePair(T left, U right) {
16+
if (left == null)
17+
throw new IllegalArgumentException("Left is NULL");
18+
if (right == null)
19+
throw new IllegalArgumentException("Right is NULL");
20+
21+
this.left = left;
22+
this.right = right;
23+
}
24+
25+
public T getLeft() {
26+
return left;
27+
}
28+
29+
public U getRight() {
30+
return right;
31+
}
32+
33+
@Override
34+
public String toString() {
35+
return "<" + left.toString()+ "," + right.toString() + ">";
36+
}
37+
38+
@Override
39+
public boolean equals(Object other) {
40+
if (!(other instanceof ImmutablePair<?,?>))
41+
return false;
42+
43+
if (this == other)
44+
return true;
45+
46+
ImmutablePair<?,?> o = (ImmutablePair<?, ?>) other;
47+
return left.equals(o.getLeft()) && right.equals(o.getRight());
48+
}
49+
50+
@Override
51+
public int hashCode() {
52+
return new HashCodeBuilder(19, 85)
53+
.append(left)
54+
.append(right)
55+
.toHashCode();
56+
}
57+
58+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package communitycommons;
2+
3+
import com.mendix.core.Core;
4+
import com.mendix.logging.ILogNode;
5+
import communitycommons.proxies.LogLevel;
6+
import communitycommons.proxies.LogNodes;
7+
import java.util.Date;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
11+
public class Logging {
12+
13+
private static Map<String, Long> timers = new HashMap<String, Long>();
14+
15+
public static void trace(String lognode, String message) {
16+
log(lognode, LogLevel.Trace, message, null);
17+
}
18+
19+
public static void info(String lognode, String message) {
20+
log(lognode, LogLevel.Info, message, null);
21+
}
22+
23+
public static void debug(String lognode, String message) {
24+
log(lognode, LogLevel.Debug, message, null);
25+
}
26+
27+
public static void warn(String lognode, String message, Throwable e) {
28+
log(lognode, LogLevel.Debug, message, e);
29+
}
30+
31+
public static void warn(String lognode, String message) {
32+
warn(lognode, message, null);
33+
}
34+
35+
public static void error(String lognode, String message, Throwable e) {
36+
log(lognode, LogLevel.Error, message, e);
37+
}
38+
39+
public static void error(String lognode, String message) {
40+
error(lognode, message, null);
41+
}
42+
43+
public static void critical(String lognode, String message, Throwable e) {
44+
log(lognode, LogLevel.Critical, message, e);
45+
}
46+
47+
public static void log(String lognode, LogLevel loglevel, String message, Throwable e) {
48+
ILogNode logger = createLogNode(lognode);
49+
switch (loglevel) {
50+
case Critical:
51+
logger.critical(message, e);
52+
break;
53+
case Warning:
54+
logger.warn(message, e);
55+
break;
56+
case Debug:
57+
logger.debug(message);
58+
break;
59+
case Error:
60+
logger.error(message, e);
61+
break;
62+
case Info:
63+
logger.info(message);
64+
break;
65+
case Trace:
66+
logger.trace(message);
67+
break;
68+
}
69+
}
70+
71+
public static Long measureEnd(String timerName, LogLevel loglevel,
72+
String message) {
73+
Long cur = new Date().getTime();
74+
if (!timers.containsKey(timerName)) {
75+
throw new IllegalArgumentException(String.format("Timer with key %s not found", timerName));
76+
}
77+
Long timeTaken = cur - timers.get(timerName);
78+
String time = String.format("%d", timeTaken);
79+
log(LogNodes.CommunityCommons.name(), loglevel, "Timer " + timerName + " finished in " + time + " ms. " + message, null);
80+
return timeTaken;
81+
}
82+
83+
public static void measureStart(String timerName) {
84+
timers.put(timerName, new Date().getTime());
85+
}
86+
87+
public static ILogNode createLogNode(String logNode) {
88+
return Core.getLogger(logNode);
89+
}
90+
}

0 commit comments

Comments
 (0)