Twitter demo in Java. (#34)

This commit is contained in:
Artur Souza 2020-11-23 15:51:25 -08:00 committed by GitHub
parent e6255c4c1a
commit d61d433ea3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1520 additions and 0 deletions

View File

@ -18,6 +18,7 @@ This demo illustrates the simplicity of [Dapr](https://github.com/dapr/dapr) on
* **Demo 1** - local development showing the speed with which developers can start and the use of Dapr components (Twitter and state)
* **Demo 2** - expands on Demo 1 and adds service invocation using both, direct invocation and consumption of events across applications using PubSub
* **Demo 3** - takes Demo 2 and illustrates how platform agnostic Dapr really is by containerizing these applications without any changes and deploying them onto Kubernetes. This demo also showcases the pluggability of components (state backed by Azure Table Storage, pubsub by Azure Service Bus)
* **Java Demo** - Implements [a similar scenario in Java](javademo/README.md), using Dapr's Java SDK.
![](images/overview.png)

View File

@ -0,0 +1,7 @@
**/secrets.json
**/.classpath
**/.settings
**/.DS_Store
**/target
**/.factorypath
**/.project

View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

View File

@ -0,0 +1,105 @@
# Twitter Sentiment Processor
>Note: this demo uses Dapr v0.11.3 and may break if different Dapr versions are used
## Sample info
| Attribute | Details |
|--------|--------|
| Dapr runtime version | v0.11.3 |
| Dapr Java SDK version | v0.9.2 |
| Language | Java |
| Environment | Local |
## Overview
This demo is an implementation of the Twitter Processing Pipeline demo in Java, using Dapr's Java SDK. The purpose of this demo is to showcase Dapr in Java.
![Overview](overview.png)
## Requirements
* Java 11 or above.
* Apache Maven 3.6 or above.
* [Twitter API credentials](https://developer.twitter.com/en/docs/basics/getting-started)
* Endpoint and Key for [Azure's Text Analytics](https://azure.microsoft.com/en-us/services/cognitive-services/text-analytics/) in Azure Cognitive Services.
Twitter credentials will have to set in a new file `provider/secrets.json`:
```json
{
"Twitter": {
"ConsumerKey": "xxxxxxxx",
"ConsumerSecret": "xxxxxxxxxx",
"AccessToken": "xxxxxxxxx",
"AccessSecret": "xxxxxxxxx"
}
}
```
Endpoint and Key for Azure's Text Analytics will have to set in a new file `processor/secrets.json`:
```json
{
"Azure": {
"CognitiveServices": {
"Endpoint": "https://xxxxxxxx.cognitiveservices.azure.com/",
"SubscriptionKey": "xxxxxxxxxxx"
}
}
}
```
## Undersanding the Pipeline
All the applications described here uses Spring Boot.
### Provider
Provider service will receive Twitter feeds, invoke the processor application above, then save the results to state store and finally publishes to a topic. This application uses multiple Dapr building blocks:
* Bindings - is used to receive Twitter feed as defined in [provider/components/binding.yaml](provider/components/binding.yaml)
* Secret Store - is used to serve secrets to the Twitter feed config, see it defined in [provider/components/secretstore.yaml](provider/components/secretstore.yaml)
* Service Invocation - is used to invoke the sentiment analysis API in the Processor application defined later on.
* State Store - is used to save tweets to Redis, as defined in [provider/components/statestore.yaml](provider/components/statestore.yaml)
* Pub Sub - is used to publish tweets to be consumed by the Web UI. PubSub is defined in [provider/components/pubsub.yaml](provider/components/pubsub.yaml)
The controller is implemented in [ApplicationController.java](provider/src/main/java/io/dapr/apps/twitter/processor/ApplicationController.java). It receives a tweet via input binding, then uses Service Invocation to get the sentiment analysis for the Tweet's text, saves the analyized tweet into the state store and, finally, publishes it to a PubSub topic.
### Processor
Processor service will receive a text and a language code and invoke Azure's Text Analytics to extract the sentiment and score for the text. If the analysis cannot be done, it will return `unknown` sentiment with a score of `0`. It only uses secret store from Dapr's building blocks, as seen in [processor/components/secretstore.yaml](processor/components/secretstore.yaml).
The controller is implemented in [ApplicationController.java](processor/src/main/java/io/dapr/apps/twitter/processor/ApplicationController.java). It does not include any Dapr specific code, it simply invokes the Azure Cognitive Services API and returns a sentiment score. The only class that makes this application use Dapr is [DaprConfig.java](processor/src/main/java/io/dapr/apps/twitter/processor/DaprConfig.java), since it is used to inject the secrets needed for the communication to the Azure Cognitive Services API.
### Viewer
Viewer app runs a static website from the [viewer/src/main/resources/static](viewer/src/main/resources/static) folder. The website stablishes a Web Socket connection to the `/ws/` endpoint. The server side for the Web Socket is handled by [SocketTextHandler.java](viewer/src/main/java/io/dapr/apps/twitter/viewer/SocketTextHandler.java). The PubSub subscription is handled by [ApplicationController.java](viewer/src/main/java/io/dapr/apps/twitter/processor/ApplicationController.java), using a singleton instance of [WebSocketPubSub.java](viewer/src/main/java/io/dapr/apps/twitter/processor/WebSocketPubSub.java) to send the topic's message to all active Web Sockets.
## Running
First, run the processor app, using `sentiment_processor` as the app id:
```sh
cd processor
mvn clean install
dapr run --app-id sentiment_processor --components-path=./components/ --app-port=8081 -- java -jar target/app.jar
```
In another terminal, run the provider app:
```sh
cd provider
mvn clean install
dapr run --components-path=./components/ --app-port=8080 -- java -jar target/app.jar
```
Finally, open another terminal and run the web UI app:
```sh
cd viewer
mvn clean install
dapr run --components-path=./components/ --app-port=8088 -- java -jar target/app.jar
```
Now, open the browser on [http://localhost:8088/](http://localhost:8088/). You should see a web UI like this:
![Web UI](ui.png)
Thanks.

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -0,0 +1,21 @@
# build stage build the jar with all our resources
FROM maven:3-openjdk-11 as build
VOLUME /tmp
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
ADD src/ /build/src/
RUN mvn package
# package stage
FROM gcr.io/distroless/java:11
ARG JAR_FILE
COPY --from=build /build/target/app.jar /opt/app.jar
WORKDIR /opt/
EXPOSE 3000
CMD [ "app.jar", "--server.port=3000" ]

View File

@ -0,0 +1,9 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: secretstore
spec:
type: secretstores.local.file
metadata:
- name: secretsFile
value: ./secrets.json

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<groupId>io.dapr.apps.twitter.processor</groupId>
<artifactId>twitter-sentiment-provider</artifactId>
<version>0.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>twitter-sentiment-processor</name>
<description>Twitter Sentiment Processor</description>
<properties>
<dapr-sdk.version>0.9.2</dapr-sdk.version>
</properties>
<repositories>
<repository>
<id>oss-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
<repository>
<id>oss-release</id>
<url>https://oss.sonatype.org/content/repositories/releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-springboot</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.processor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = { "io.dapr.apps.twitter.processor", "io.dapr.springboot" })
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.processor;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.Optional;
import javax.net.ssl.HttpsURLConnection;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.dapr.apps.twitter.processor.model.Sentiment;
import io.dapr.apps.twitter.processor.model.Text;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@RestController
@Slf4j
@RequiredArgsConstructor
public class ApplicationController {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final JsonFactory JSON_FACTORY = new JsonFactory();
private static final String PATH = "/text/analytics/v3.0/sentiment";
@Autowired
@Qualifier("endpoint")
private final String endpoint;
@Qualifier("subscriptionKey")
private final String subscriptionKey;
@PostMapping(value = "/sentiment")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Sentiment tweet(@RequestBody Text text) throws IOException {
log.info(String.format("Text received in %s: %s", text.getLanguage(), text.getText()));
assert(endpoint != null);
assert(subscriptionKey != null);
URL url = new URL(endpoint+PATH);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "text/json");
connection.setRequestProperty("Ocp-Apim-Subscription-Key", subscriptionKey);
connection.setDoOutput(true);
writeRequest(text, connection.getOutputStream());
JsonNode node = OBJECT_MAPPER.readTree(connection.getInputStream());
String sentiment = Optional.ofNullable(node)
.map(n -> n.get("documents"))
.map(n -> n.get(0))
.map(n -> n.get("sentiment"))
.map(n -> n.asText())
.orElse("unknown");
float score = Optional.ofNullable(node)
.map(n -> n.get("documents"))
.map(n -> n.get(0))
.map(n -> n.get("confidenceScores"))
.map(n -> n.get(sentiment))
.map(n -> n.floatValue())
.orElse((float) 0);
return new Sentiment(sentiment, score);
}
private static void writeRequest(Text text, OutputStream output) throws IOException {
try (OutputStream bos = new BufferedOutputStream(output)) {
try (JsonGenerator generator = JSON_FACTORY.createGenerator(bos)) {
generator.writeStartObject();
generator.writeArrayFieldStart("documents");
generator.writeStartObject();
generator.writeStringField("id", "1");
generator.writeStringField("language", text.getLanguage());
generator.writeStringField("text", text.getText());
generator.writeEndObject();
generator.writeEndArray();
generator.writeEndObject();
bos.flush();
}
}
}
@GetMapping(path = "/health")
public Mono<Void> health() {
return Mono.empty();
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.processor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
public class DaprConfig {
private static final DaprClientBuilder BUILDER = new DaprClientBuilder();
@Bean(name = "endpoint")
public String fetchEndpoint() {
return fetchSecret("Azure:CognitiveServices:Endpoint");
}
@Bean(name = "subscriptionKey")
public String fetchSubscriptionKey() {
return fetchSecret("Azure:CognitiveServices:SubscriptionKey");
}
private static String fetchSecret(String secret) {
return buildDaprClient().getSecret("secretstore", secret).block().values().iterator().next();
}
private static DaprClient buildDaprClient() {
log.info("Creating a new Dapr Client");
return BUILDER.build();
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.processor.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Sentiment {
@JsonProperty("sentiment")
String sentiment;
@JsonProperty("confidence")
float confidence;
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.processor.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Text {
@JsonProperty("text")
String text;
@JsonProperty("lang")
String language;
}

View File

@ -0,0 +1,3 @@
logging.level.root=INFO
spring.application.name=SentimentProcessor
server.port=8081

View File

@ -0,0 +1,21 @@
# build stage build the jar with all our resources
FROM maven:3-openjdk-11 as build
VOLUME /tmp
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
ADD src/ /build/src/
RUN mvn package
# package stage
FROM gcr.io/distroless/java:11
ARG JAR_FILE
COPY --from=build /build/target/app.jar /opt/app.jar
WORKDIR /opt/
EXPOSE 3000
CMD [ "app.jar", "--server.port=3000" ]

View File

@ -0,0 +1,24 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: tweet
auth:
secretStore: secretstore
spec:
type: bindings.twitter
metadata:
- name: consumerKey
secretKeyRef:
name: Twitter:ConsumerKey # twitter api consumer key, required
- name: consumerSecret
secretKeyRef:
name: Twitter:ConsumerSecret # twitter api consumer secret, required
- name: accessToken
secretKeyRef:
name: Twitter:AccessToken # twitter api access token, required
- name: accessSecret
secretKeyRef:
name: Twitter:AccessSecret # twitter api access secret, required
- name: query
value: "cat" # your search query, required

View File

@ -0,0 +1,11 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: messagebus
spec:
type: pubsub.redis
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""

View File

@ -0,0 +1,9 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: secretstore
spec:
type: secretstores.local.file
metadata:
- name: secretsFile
value: ./secrets.json

View File

@ -0,0 +1,11 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<groupId>io.dapr.apps.twitter.provider</groupId>
<artifactId>twitter-provider</artifactId>
<version>0.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>twitter-provider</name>
<description>Twitter Provider</description>
<properties>
<dapr-sdk.version>0.9.2</dapr-sdk.version>
</properties>
<repositories>
<repository>
<id>oss-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
<repository>
<id>oss-release</id>
<url>https://oss.sonatype.org/content/repositories/releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-springboot</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = { "io.dapr.apps.twitter.provider", "io.dapr.springboot" })
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.dapr.apps.twitter.provider.model.AnalyzedTweet;
import io.dapr.apps.twitter.provider.model.Sentiment;
import io.dapr.apps.twitter.provider.model.Tweet;
import io.dapr.client.DaprClient;
import io.dapr.client.domain.HttpExtension;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@RestController
@Slf4j
@RequiredArgsConstructor
public class ApplicationController {
private static final String SENTIMENT_PROCESSOR_APP = "sentiment_processor";
private static final String STATE_STORE = "statestore";
private static final String PUBSUB = "messagebus";
private static final String PUBSUB_TOPIC = "tweets";
@Autowired
private final DaprClient daprClient;
@PostMapping(value = "/tweet")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Mono<Void> tweet(@RequestBody Tweet tweet) {
log.info(String.format("Tweet received %s in %s: %s", tweet.getId(), tweet.getLanguage(), tweet.getText()));
return daprClient
.invokeService(SENTIMENT_PROCESSOR_APP, "sentiment", tweet, HttpExtension.POST, Sentiment.class)
.map(sentiment -> AnalyzedTweet.builder()
.id(tweet.getId())
.tweet(tweet)
.sentiment(sentiment)
.build())
.flatMap(analizedTweet -> daprClient.saveState(STATE_STORE, analizedTweet.getId(), analizedTweet)
.then(daprClient.publishEvent(PUBSUB, PUBSUB_TOPIC, analizedTweet)
.thenReturn(analizedTweet)))
.doOnSuccess(analizedTweet -> log.info(String.format("Tweet saved %s: %s", analizedTweet.getId(), analizedTweet.getSentiment().getSentiment())))
.doOnError(onError -> log
.error(String.format("Tweet not saved %s : %s", tweet.getId(), tweet.getText()), onError))
.then();
}
@GetMapping(path = "/health")
public Mono<Void> health() {
return Mono.empty();
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
public class DaprConfig {
private static final DaprClientBuilder BUILDER = new DaprClientBuilder();
@Bean
public DaprClient buildDaprClient() {
log.info("Creating a new Dapr Client");
return BUILDER.build();
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider.model;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AnalyzedTweet {
@JsonProperty("id")
String id;
@JsonProperty("tweet")
Tweet tweet;
@JsonProperty("sentiment")
Sentiment sentiment;
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Sentiment {
@JsonProperty("sentiment")
String sentiment;
@JsonProperty("confidence")
float confidence;
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Tweet {
@JsonProperty("id_str")
String id;
@JsonProperty("user")
TwitterUser author;
@JsonProperty("full_text")
String fullText;
@JsonProperty("text")
String text;
@JsonProperty("lang")
String language;
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.provider.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TwitterUser {
@JsonProperty("name")
String name;
@JsonProperty("screen_name")
String screenName;
@JsonProperty("profile_image_url_https")
String picture;
}

View File

@ -0,0 +1,3 @@
logging.level.root=INFO
spring.application.name=TwitterProvider
server.port=8080

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -0,0 +1,21 @@
# build stage build the jar with all our resources
FROM maven:3-openjdk-11 as build
VOLUME /tmp
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
ADD src/ /build/src/
RUN mvn package
# package stage
FROM gcr.io/distroless/java:11
ARG JAR_FILE
COPY --from=build /build/target/app.jar /opt/app.jar
WORKDIR /opt/
EXPOSE 3000
CMD [ "app.jar", "--server.port=3000" ]

View File

@ -0,0 +1,11 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: messagebus
spec:
type: pubsub.redis
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""

View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<groupId>io.dapr.apps.twitter.viewer</groupId>
<artifactId>twitter-viewer</artifactId>
<version>0.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>twitter-viewer</name>
<description>Twitter Viewer</description>
<properties>
<dapr-sdk.version>0.9.2</dapr-sdk.version>
</properties>
<repositories>
<repository>
<id>oss-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
<repository>
<id>oss-release</id>
<url>https://oss.sonatype.org/content/repositories/releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-springboot</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>${dapr-sdk.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.viewer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = { "io.dapr.apps.twitter.viewer", "io.dapr.springboot" })
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.viewer;
import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@Slf4j
@RequiredArgsConstructor
@RestController
public class ApplicationController {
private static final String PUBSUB = "messagebus";
@Topic(name = "tweets", pubsubName = PUBSUB)
@PostMapping(value = "/tweets")
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public void tweet(@RequestBody byte[] payload) throws IOException {
CloudEvent event = CloudEvent.deserialize(payload);
log.info("Received cloud event: " + event.getData());
WebSocketPubSub.INSTANCE.send(event.getData());
}
@GetMapping(path = "/health")
public Mono<Void> health() {
return Mono.empty();
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.viewer;
import java.io.IOException;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SocketTextHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
WebSocketPubSub.INSTANCE.registerSession(session);
}
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
log.info("Sending message: " + message.getPayload());
session.sendMessage(message);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
WebSocketPubSub.INSTANCE.unregisterSession(session);
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.viewer;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SocketTextHandler(), "/ws");
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/
package io.dapr.apps.twitter.viewer;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WebSocketPubSub {
private final Map<String, WebSocketSession> sessions = Collections.synchronizedMap(new HashMap<>());
public static final WebSocketPubSub INSTANCE = new WebSocketPubSub();
private WebSocketPubSub() {}
public void registerSession(WebSocketSession session) {
log.info("Registering new websocketsession: " + session.getId());
sessions.put(session.getId(), session);
}
public void unregisterSession(WebSocketSession session) {
log.info("unregistering new websocketsession: " + session.getId());
sessions.remove(session.getId());
}
public void send(String content) {
sessions.values().forEach(socket -> {
try {
socket.sendMessage(new TextMessage(content));
} catch (IOException e) {
log.error("Could not send message to websocket.", e);
}
});
}
}

View File

@ -0,0 +1,3 @@
logging.level.root=INFO
spring.application.name=TwitterViewer
server.port=8088

View File

@ -0,0 +1,131 @@
/*
Knative colors
Dark Blue: #0865ad
Light Blue: #6695ca
*/
html,
body {
height: 100%;
margin: 0;
padding: 0;
background-color: #fff;
}
#wrapper {
padding: 0;
margin: 0;
height: 100%;
text-align: center;
widows: 100%;
}
#page-header {
padding: 0;
margin: 10px 0 0 0;
clear: both;
break-after: always;
height: 100px;
}
#page-header-image {
border: 0;
margin-left: 25px;
}
#page-header-image img {
width: 100px;
height: 100px;
float: left;
}
#connection {
float: right;
margin: 10px;
font-family: Geneva, Verdana, sans-serif;
font-size: 1em;
}
#middle-section {
margin: 0;
padding: 10px;
height: 700px;
overflow: auto;
}
#middle-section div {
margin: 5px;
}
#page-footer {
widows: 80%;
margin: 30px 0 0 0;
font-family: Geneva, Verdana, sans-serif;
font-size: 1em;
}
#tweets {
background-color: #20329b;
padding: 0;
margin: 5px;
height: 690px;
overflow: auto;
}
img.profile-pic {
width: 72px;
height: 72px;
margin: 0 5px 5px 0;
float: left;
}
div.item-text {
margin-left: 10px;
padding-left: 10px;
}
#tweets b {
font-family: Geneva, Verdana, sans-serif;
font-size: 1em;
color: #20329b;
margin: 0;
}
#tweets i {
font-family: Geneva, Verdana, sans-serif;
font-size: 0.8em;
font-style: normal;
}
#tweets i.small {
font-size: 0.7em;
color: #666666;
}
img.sentiment {
width: 18px;
height: 18px;
margin: 0 3px 0 0;
vertical-align: baseline;
}
img.tweet-link {
width: 25px;
height: 25px;
margin: 0 0 0 3px;
vertical-align: baseline;
}
.error {
color: red;
}
.item {
clear:both;
padding: 5px;
text-align: left;
background-color: #fff;
overflow: auto;
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="367px" height="270px" viewBox="0 0 367 270" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>Artboard</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M89.7917119,193.508761 L62.9000392,193.508761 L62.9000392,185.06412 C60.3311064,188.28895 57.7348835,190.639215 55.1112926,192.114985 C50.5200084,194.683918 45.3002339,195.968365 39.4518124,195.968365 C29.9959534,195.968365 21.5787254,192.716254 14.1998759,186.211935 C5.39991464,178.450478 1,168.174901 1,155.384895 C1,142.376257 5.50922929,131.991365 14.5278231,124.229909 C21.6880401,118.053538 29.9139674,114.9654 39.205852,114.9654 C44.6170083,114.9654 49.7001395,116.113203 54.4553981,118.408845 C57.1883053,119.720641 60.0031576,121.879605 62.9000392,124.885803 L62.9000392,70.3645766 L89.7917119,70.3645766 L89.7917119,193.508761 Z M63.7199073,155.466882 C63.7199073,150.656965 62.0255302,146.57133 58.6367253,143.209855 C55.2479203,139.848379 51.148621,138.167666 46.3387042,138.167666 C40.9822061,138.167666 36.5822914,140.189987 33.1388283,144.23469 C30.3512629,147.514178 28.9575012,151.258205 28.9575012,155.466882 C28.9575012,159.675559 30.3512629,163.419586 33.1388283,166.699075 C36.5276333,170.743777 40.9275479,172.766098 46.3387042,172.766098 C51.2032791,172.766098 55.3162428,171.09905 58.6777187,167.764903 C62.0391946,164.430756 63.7199073,160.331457 63.7199073,155.466882 Z M190.65006,193.508761 L163.758387,193.508761 L163.758387,185.06412 C161.189454,188.28895 158.593231,190.639215 155.969641,192.114985 C151.378356,194.683918 146.158582,195.968365 140.31016,195.968365 C130.854301,195.968365 122.437073,192.716254 115.058224,186.211935 C106.258263,178.450478 101.858348,168.174901 101.858348,155.384895 C101.858348,142.376257 106.367577,131.991365 115.386171,124.229909 C122.546388,118.053538 130.772315,114.9654 140.0642,114.9654 C145.475356,114.9654 150.558487,116.113203 155.313746,118.408845 C158.046653,119.720641 160.861506,121.879605 163.758387,124.885803 L163.758387,117.425004 L190.65006,117.425004 L190.65006,193.508761 Z M164.578255,155.466882 C164.578255,150.656965 162.883878,146.57133 159.495073,143.209855 C156.106268,139.848379 152.006969,138.167666 147.197052,138.167666 C141.840554,138.167666 137.440639,140.189987 133.997176,144.23469 C131.209611,147.514178 129.815849,151.258205 129.815849,155.466882 C129.815849,159.675559 131.209611,163.419586 133.997176,166.699075 C137.385981,170.743777 141.785896,172.766098 147.197052,172.766098 C152.061627,172.766098 156.174591,171.09905 159.536067,167.764903 C162.897543,164.430756 164.578255,160.331457 164.578255,155.466882 Z M294.54192,155.548869 C294.54192,168.557507 290.03269,178.942399 281.014097,186.703856 C273.85388,192.880226 265.627952,195.968365 256.336068,195.968365 C250.924911,195.968365 245.84178,194.820561 241.086522,192.524919 C238.353614,191.213123 235.538762,189.054159 232.64188,186.047961 L232.64188,231.550639 L205.750208,231.550639 L205.750208,117.425004 L232.64188,117.425004 L232.64188,125.869645 C235.046839,122.699473 237.643062,120.349208 240.430627,118.81878 C245.021911,116.249847 250.241686,114.9654 256.090107,114.9654 C265.545966,114.9654 273.963194,118.21751 281.342044,124.72183 C290.142005,132.483286 294.54192,142.758863 294.54192,155.548869 Z M266.584418,155.466882 C266.584418,151.148889 265.217985,147.404862 262.485078,144.23469 C259.041615,140.189987 254.614372,138.167666 249.203215,138.167666 C244.338641,138.167666 240.225677,139.834714 236.864201,143.168861 C233.502725,146.503008 231.822012,150.602307 231.822012,155.466882 C231.822012,160.276799 233.516389,164.362434 236.905194,167.72391 C240.293999,171.085386 244.393299,172.766098 249.203215,172.766098 C254.614372,172.766098 259.014286,170.743777 262.403091,166.699075 C265.190657,163.419586 266.584418,159.675559 266.584418,155.466882 Z M363.671373,142.267006 C359.899961,140.463288 356.073949,139.561442 352.19322,139.561442 C343.338601,139.561442 337.599582,143.168825 334.975991,150.3837 C333.992144,153.007291 333.500228,156.532689 333.500228,160.959998 L333.500228,193.508761 L306.608556,193.508761 L306.608556,117.425004 L333.500228,117.425004 L333.500228,129.886998 C336.342452,125.459689 339.403262,122.262235 342.682751,120.294542 C347.11006,117.670951 352.357164,116.359175 358.424218,116.359175 C359.845329,116.359175 361.594364,116.441161 363.671373,116.605136 L363.671373,142.267006 Z" id="dapr" fill="#0D2192"></path>
<polygon id="tie" fill="#0D2192" fill-rule="nonzero" points="205.538409 194.062172 232.614551 194.062172 234.946621 257.633831 219.07648 268.75443 203.206339 257.633831"></polygon>
<rect id="Rectangle-4" fill="#0D2192" fill-rule="nonzero" x="144.829497" y="2.27908829" width="102.722643" height="72.2941444" rx="2"></rect>
<rect id="Rectangle-4" fill="#FFFFFF" fill-rule="nonzero" opacity="0.0799999982" x="144.829497" y="2.27908829" width="37.9976369" height="72.2941444"></rect>
<rect id="Rectangle-3" fill="#0D2192" fill-rule="nonzero" x="112.390768" y="69.9090944" width="166.248488" height="17.3513412" rx="3.72016"></rect>
<rect id="Rectangle-4" fill="#FFFFFF" fill-rule="nonzero" opacity="0.0799999982" x="112.390768" y="69.9090944" width="51.4375478" height="21.3554969"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#f85565;}.cls-2,.cls-3{fill:#393c54;}.cls-2,.cls-7{opacity:0.2;}.cls-4{fill:#fff;}.cls-5{fill:#515570;opacity:0.1;}.cls-6,.cls-7{fill:none;stroke:#393c54;stroke-linecap:round;stroke-linejoin:round;stroke-width:4px;}</style></defs><title/><circle class="cls-1" cx="64" cy="64" r="58"/><path class="cls-2" d="M53,107A71.65,71.65,0,0,1,16.25,96.91,58,58,0,0,0,121.56,57,72,72,0,0,1,53,107Z"/><path class="cls-3" d="M90.3,76.14c1.91.5,3.51-1.74,3.28-3.7C91.77,57.54,79.22,49,64,49S36.23,57.54,34.42,72.44c-.24,2,1.37,4.23,3.28,3.7C47.92,73.26,79.41,73.31,90.3,76.14Z"/><path class="cls-4" d="M83,74.92V68.14a4,4,0,0,0-4-4H49a4,4,0,0,0-4,4v6.74A201.51,201.51,0,0,1,83,74.92Z"/><path class="cls-4" d="M83,57.14v-2.6A33,33,0,0,0,64,49a33,33,0,0,0-19,5.54v2.6a4,4,0,0,0,4,4H79A4,4,0,0,0,83,57.14Z"/><path class="cls-5" d="M45,71.88v3a201.51,201.51,0,0,1,38,0v-3A201.51,201.51,0,0,0,45,71.88Z"/><path class="cls-5" d="M64,53a33.09,33.09,0,0,1,18.78,5.38A4,4,0,0,0,83,57.14v-2.6A33,33,0,0,0,64,49a33,33,0,0,0-19,5.54v2.6a4,4,0,0,0,.22,1.25A33.09,33.09,0,0,1,64,53Z"/><path class="cls-6" d="M32,36s9-2,14,4c-5-1-12,2-15,6"/><path class="cls-6" d="M96,36s-9-2-14,4c5-1,12,2,15,6"/><line class="cls-7" x1="57" x2="71" y1="80" y2="80"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="32px" version="1.1" viewBox="0 0 32 32" width="32px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#157EFB" id="icon-1-flashed-face"><path d="M16.5,29 C23.4035597,29 29,23.4035597 29,16.5 C29,9.59644029 23.4035597,4 16.5,4 C9.59644029,4 4,9.59644029 4,16.5 C4,23.4035597 9.59644029,29 16.5,29 Z M12,16 C13.6568543,16 15,14.6568543 15,13 C15,11.3431457 13.6568543,10 12,10 C10.3431457,10 9,11.3431457 9,13 C9,14.6568543 10.3431457,16 12,16 Z M12,15 C13.1045696,15 14,14.1045696 14,13 C14,11.8954304 13.1045696,11 12,11 C10.8954304,11 10,11.8954304 10,13 C10,14.1045696 10.8954304,15 12,15 Z M21,16 C22.6568543,16 24,14.6568543 24,13 C24,11.3431457 22.6568543,10 21,10 C19.3431457,10 18,11.3431457 18,13 C18,14.6568543 19.3431457,16 21,16 Z M21,15 C22.1045696,15 23,14.1045696 23,13 C23,11.8954304 22.1045696,11 21,11 C19.8954304,11 19,11.8954304 19,13 C19,14.1045696 19.8954304,15 21,15 Z M12,14 C12.5522848,14 13,13.5522848 13,13 C13,12.4477152 12.5522848,12 12,12 C11.4477152,12 11,12.4477152 11,13 C11,13.5522848 11.4477152,14 12,14 Z M21,14 C21.5522848,14 22,13.5522848 22,13 C22,12.4477152 21.5522848,12 21,12 C20.4477152,12 20,12.4477152 20,13 C20,13.5522848 20.4477152,14 21,14 Z M14,21 L14,22 L19,22 L19,21 L14,21 Z" id="flashed-face"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#f8dc25;}.cls-2{fill:#f2bc0f;opacity:0.7;}.cls-3{fill:#393c54;}.cls-4{fill:#fff;}.cls-5,.cls-6{fill:#515570;}.cls-6{opacity:0.1;}</style></defs><title/><circle class="cls-1" cx="64" cy="64" r="58"/><path class="cls-2" d="M53,107A71.65,71.65,0,0,1,16.25,96.91,58,58,0,0,0,121.56,57,72,72,0,0,1,53,107Z"/><path class="cls-3" d="M90.3,77a3.3,3.3,0,0,1,3.28,3.7,29.76,29.76,0,0,1-59.15,0A3.3,3.3,0,0,1,37.7,77Z"/><path class="cls-4" d="M45,77H83a0,0,0,0,1,0,0v5a4,4,0,0,1-4,4H49a4,4,0,0,1-4-4V77A0,0,0,0,1,45,77Z"/><path class="cls-4" d="M45,100.21a29.52,29.52,0,0,0,38,0V100a4,4,0,0,0-4-4H49a4,4,0,0,0-4,4Z"/><circle class="cls-4" cx="39" cy="59" r="13"/><circle class="cls-5" cx="39" cy="59" r="7"/><path class="cls-1" d="M26,77c0-7.18,5.82-10,13-10s13,2.82,13,10"/><circle class="cls-4" cx="89" cy="59" r="13"/><circle class="cls-5" cx="89" cy="59" r="7"/><path class="cls-1" d="M76,77c0-7.18,5.82-10,13-10s13,2.82,13,10"/><rect class="cls-6" height="4" width="42" x="43" y="77"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" ?><svg id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#439CD6;}
</style><path class="st0" d="M196.9,462c120.3,0,186.1-99.7,186.1-186.1c0-2.8,0-5.6-0.2-8.5c12.8-9.3,23.8-20.7,32.6-33.9 c-11.9,5.3-24.6,8.8-37.6,10.3c13.7-8.2,23.9-21,28.8-36.2c-12.8,7.6-26.9,13-41.5,15.9c-24.8-26.3-66.2-27.6-92.5-2.8 c-17,16-24.2,39.8-18.9,62.5c-52.6-2.6-101.6-27.5-134.8-68.3c-17.4,29.9-8.5,68.1,20.2,87.3c-10.4-0.3-20.6-3.1-29.7-8.2 c0,0.3,0,0.5,0,0.8c0,31.1,22,57.9,52.5,64.1c-9.6,2.6-19.7,3-29.5,1.1c8.6,26.6,33.1,44.9,61.1,45.4 c-23.2,18.2-51.8,28.1-81.2,28.1c-5.2,0-10.4-0.3-15.6-0.9C126.5,451.8,161.3,462,196.9,462" id="XMLID_22_"/></svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="32px" version="1.1" viewBox="0 0 32 32" width="32px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#CCCCCC" id="icon-1-flashed-face"><path d="M16.5,29 C23.4035597,29 29,23.4035597 29,16.5 C29,9.59644029 23.4035597,4 16.5,4 C9.59644029,4 4,9.59644029 4,16.5 C4,23.4035597 9.59644029,29 16.5,29 Z M12,16 C13.6568543,16 15,14.6568543 15,13 C15,11.3431457 13.6568543,10 12,10 C10.3431457,10 9,11.3431457 9,13 C9,14.6568543 10.3431457,16 12,16 Z M12,15 C13.1045696,15 14,14.1045696 14,13 C14,11.8954304 13.1045696,11 12,11 C10.8954304,11 10,11.8954304 10,13 C10,14.1045696 10.8954304,15 12,15 Z M21,16 C22.6568543,16 24,14.6568543 24,13 C24,11.3431457 22.6568543,10 21,10 C19.3431457,10 18,11.3431457 18,13 C18,14.6568543 19.3431457,16 21,16 Z M21,15 C22.1045696,15 23,14.1045696 23,13 C23,11.8954304 22.1045696,11 21,11 C19.8954304,11 19,11.8954304 19,13 C19,14.1045696 19.8954304,15 21,15 Z M12,14 C12.5522848,14 13,13.5522848 13,13 C13,12.4477152 12.5522848,12 12,12 C11.4477152,12 11,12.4477152 11,13 C11,13.5522848 11.4477152,14 12,14 Z M21,14 C21.5522848,14 22,13.5522848 22,13 C22,12.4477152 21.5522848,12 21,12 C20.4477152,12 20,12.4477152 20,13 C20,13.5522848 20.4477152,14 21,14 Z M14,21 L14,22 L19,22 L19,21 L14,21 Z" id="flashed-face"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<!--
Original demo: https://github.com/mchmarny/dapr-demos/pipeline
-->
<html lang="en">
<head>
<title>dapr event viewer</title>
<meta charset="UTF-8">
<meta name="description" content="dapr event viewer">
<meta name="keywords" content="events, dapr, websocket, golang">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="img/favicon.ico" rel="shortcut icon" />
<link rel="stylesheet" href="css/app.css" />
<script type="text/javascript" src="js/app.js"></script>
</head>
<body>
<div id="wrapper">
<!-- Header -->
<div id="page-header">
<div id="page-header-image">
<img src="img/dapr.svg" />
</div>
<div id="connection">connection: <b id="connection-status"></b></div>
</div>
<div id="middle-section">
<div id="tweets"></div>
</div>
<!-- Footer -->
<div id="page-footer">
<p>
<a href="https://github.com/dapr/samples">
Dapr Samples
</a>
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,76 @@
window.onload = function () {
console.log("Protocol: " + location.protocol);
var wsURL = "ws://" + document.location.host + "/ws"
if (location.protocol == 'https:') {
wsURL = "wss://" + document.location.host + "/ws"
}
console.log("WS URL: " + wsURL);
var log = document.getElementById("tweets");
function appendLog(item) {
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
if (log) {
sock = new WebSocket(wsURL);
var connDiv = document.getElementById("connection-status");
connDiv.innerText = "closed";
sock.onopen = function () {
console.log("connected to " + wsURL);
connDiv.innerText = "open";
};
sock.onclose = function (e) {
console.log("connection closed (" + e.code + ")");
connDiv.innerText = "closed";
};
sock.onmessage = function (e) {
console.log(e);
var t = JSON.parse(e.data);
console.log(t);
var scoreStr = "neutral";
var scoreAlt = "neutral: 0"
if (t.hasOwnProperty("sentiment")) {
console.log(t.sentiment);
if (t.sentiment.sentiment.length > 0) {
scoreStr = t.sentiment.sentiment;
scoreAlt = scoreStr + ": " + t.sentiment.confidence;
}
}
var tweetText = t.tweet.text;
if(t.tweet.fullText != null) {
tweetText = t.tweet.full_text;
}
var item = document.createElement("div");
item.className = "item";
var postURL = t.tweet.user.name;
if (t.tweet.user.screen_name) {
postURL = t.tweet.user.screen_name +
"<a href='https://twitter.com/" + t.tweet.user.screen_name + "/status/" + t.tweet.id_str +
"' target='_blank'><img src='img/tw.svg' class='tweet-link' /></a></b>";
}
var tmsg = "<img src='" + t.tweet.user.profile_image_url_https + "' class='profile-pic' />" +
"<div class='item-text'><b><img src='img/" + scoreStr +
".svg' title='" + scoreAlt + "' class='sentiment' />" + postURL +
"<br /><i>" + tweetText + "</i></div>";
item.innerHTML = tmsg
appendLog(item);
};
}
};