Skip to content

Commit

Permalink
Release 0.1.0 (#30)
Browse files Browse the repository at this point in the history
- Handle incoming attachments
  • Loading branch information
levimdmiller authored Feb 16, 2021
1 parent 27676c9 commit 9f56731
Show file tree
Hide file tree
Showing 15 changed files with 468 additions and 13 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# sms-bridge
Matrix Sms Bridge
Matrix SMS Bridge

Bridges the sms service Twilio to matrix, but could be extended to other services/chat servers
Bridges SMS to matrix.


Run App
Expand Down Expand Up @@ -52,7 +52,7 @@ app_service_config_files:
- "/path/to/appservice/registration.yaml"
```

### Sms Bridge Database Setup:
### SMS Bridge Database Setup:
Create a database called sms_bridge (with appropriate users/roles if required)
E.g., in postgres:
```
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
</parent>
<groupId>ca.levimiller</groupId>
<artifactId>sms-bridge</artifactId>
<version>0.0.8-SNAPSHOT</version>
<version>0.1.0</version>
<name>sms-bridge</name>
<description>Sms Bridge for Matrix</description>

<properties>
<java.version>11</java.version>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
<lombok.version>1.18.10</lombok.version>
<jersey>2.30</jersey>
</properties>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/ca/levimiller/smsbridge/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand Down Expand Up @@ -40,4 +41,9 @@ public CommonsRequestLoggingFilter requestLoggingFilter() {
public ObjectMapper mapper() {
return new JacksonContextResolver().getContext(null);
}

@Bean
public FormHttpMessageConverter formHttpMessageConverter() {
return new FormHttpMessageConverter();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ca.levimiller.smsbridge.data.dto;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;

@Component
public class TwilioSmsDtoConverter extends AbstractHttpMessageConverter<TwilioSmsDto> {
private final FormHttpMessageConverter formHttpMessageConverter;

@Inject
public TwilioSmsDtoConverter(FormHttpMessageConverter formHttpMessageConverter) {
super(new MediaType("application","x-www-form-urlencoded", StandardCharsets.UTF_8));
this.formHttpMessageConverter = formHttpMessageConverter;
}

@Override
protected boolean supports(Class<?> clazz) {
return TwilioSmsDto.class == clazz;
}

@Override
protected TwilioSmsDto readInternal(Class<? extends TwilioSmsDto> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
Map<String, String> vals = formHttpMessageConverter.read(null, inputMessage).toSingleValueMap();

Integer numMedia = getInteger(vals, "NumMedia");

return TwilioSmsDto.builder()
.messageSid(vals.get("MessageSid"))
.messageSid(vals.get("MessageSid"))
.accountSid(vals.get("AccountSid"))
.from(vals.get("From"))
.to(vals.get("To"))
.body(vals.get("Body"))
.numSegments(getInteger(vals, "NumSegments"))
.numMedia(numMedia)
.mediaContentTypes(getList(vals, "MediaContentType", numMedia))
.mediaUrls(getList(vals, "MediaUrl", numMedia))
.messagingServiceSid(vals.get("MessagingServiceSid"))
.fromCity(vals.get("FromCity"))
.fromState(vals.get("FromState"))
.fromZip(vals.get("FromZip"))
.fromCountry(vals.get("FromCountry"))
.toCity(vals.get("ToCity"))
.toState(vals.get("ToState"))
.toZip(vals.get("ToZip"))
.toCountry(vals.get("ToCountry"))
.build();
}

@Override
protected void writeInternal(TwilioSmsDto twilioSmsDto, HttpOutputMessage httpOutputMessage)
throws HttpMessageNotWritableException {
// TwilioSmsDto is currently only for incoming requests.
}

private Integer getInteger(Map<String, String> vals, String key) {
try {
return Integer.parseInt(vals.get(key));
} catch (NumberFormatException e) {
return null;
}
}

private List<String> getList(Map<String, String> vals, String key, Integer size) {
if (size == null) {
return Collections.emptyList();
}
List<String> result = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
result.add(vals.get(key + i));
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import javax.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -15,10 +17,13 @@
@Api(value = "/twilio", tags = "Api for requests from Twilio")
public interface TwilioController {

@PostMapping("/sms")
@PostMapping(
value = "/sms",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
@ApiOperation("Create an sms message")
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Created", response = String.class),
@ApiResponse(code = 400, message = "Request not valid")})
void createSms(@Valid TwilioSmsDto sms);
void createSms(@Valid @RequestBody TwilioSmsDto sms);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public TwilioApi(
public void createSms(TwilioSmsDto sms) {
log.debug("Received sms: {}", sms);
Message message = messageTransformer.transform(sms);
if (message.getMedia() != null) {
message.getMedia().forEach(media -> media.setMessage(message));
}
messageService.save(message);
chatService.sendMessage(message);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ca.levimiller.smsbridge.service;

import ca.levimiller.smsbridge.data.model.ChatUser;
import ca.levimiller.smsbridge.data.model.Media;

public interface AttachmentService {

/**
* Sends the given attachment.
* @param attachment - attachment to send
*/
void sendAttachment(ChatUser user, String roomId, Media attachment);

/**
* Returns true if the given content type is supported by the attachment service.
* @param contentType - content type to check
* @return - true if supported
*/
boolean supportsType(String contentType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import ca.levimiller.smsbridge.data.model.ChatUser;
import ca.levimiller.smsbridge.data.model.Message;
import ca.levimiller.smsbridge.error.NotFoundException;
import ca.levimiller.smsbridge.service.AttachmentService;
import ca.levimiller.smsbridge.service.ChatService;
import ca.levimiller.smsbridge.service.RoomService;
import ca.levimiller.smsbridge.service.UserService;
import io.github.ma1uta.matrix.client.AppServiceClient;
import javax.inject.Inject;
import liquibase.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
Expand All @@ -22,16 +24,19 @@ public class MatrixChatService implements ChatService {
private final RoomService roomService;
private final UserService userService;
private final AppServiceClient matrixClient;
private final AttachmentService attachmentService;

@Inject
public MatrixChatService(
ChatUserRepository chatUserRepository,
RoomService roomService, UserService userService,
AppServiceClient matrixClient) {
AppServiceClient matrixClient,
AttachmentService attachmentService) {
this.chatUserRepository = chatUserRepository;
this.roomService = roomService;
this.userService = userService;
this.matrixClient = matrixClient;
this.attachmentService = attachmentService;
}

@Override
Expand All @@ -46,10 +51,20 @@ public void sendMessage(Message message) {
// Get room id and user id (ensure created/joined/etc.)
ChatUser from = userService.getUser(message.getFromContact());
String roomId = roomService.getRoom(to, from);
matrixClient.userId(from.getOwnerId()).event().sendMessage(roomId, message.getBody())
.exceptionally(throwable -> {
log.error("Error sending message to matrix: ", throwable);
return null;
});

if (!StringUtils.isEmpty(message.getBody())) {
matrixClient.userId(from.getOwnerId()).event().sendMessage(roomId, message.getBody())
.exceptionally(throwable -> {
log.error("Error sending message to matrix: ", throwable);
return null;
});
}

// send attachments
if (message.getMedia() != null) {
message.getMedia().forEach(media -> {
attachmentService.sendAttachment(from, roomId, media);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ca.levimiller.smsbridge.service.impl.matrix.attachment;

import ca.levimiller.smsbridge.data.model.ChatUser;
import ca.levimiller.smsbridge.data.model.Media;
import ca.levimiller.smsbridge.service.AttachmentService;
import io.github.ma1uta.matrix.client.AppServiceClient;
import io.github.ma1uta.matrix.event.content.EventContent;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;

@Slf4j
public abstract class AbstractAttachmentService implements AttachmentService {
protected final AppServiceClient matrixClient;

public AbstractAttachmentService(AppServiceClient matrixClient) {
this.matrixClient = matrixClient;
}

/**
* Builds the event content from the attachment.
* @param attachment - attachment to send
* @return - event content for attachment
*/
protected abstract EventContent getContent(ChatUser user, Media attachment);

@Override
public void sendAttachment(ChatUser user, String roomId, Media attachment) {
matrixClient.userId(user.getOwnerId()).event()
.sendEvent(roomId, "m.room.message", getContent(user, attachment))
.exceptionally(throwable -> {
log.error(
String.format("Error sending attachment to matrix (%s): ", getClass()),
throwable);
return null;
});
}

/**
* Tries to upload the file at the given url to the matrix homeserver.
* <p></p>
* If an exception occurs during the upload, the original url is returned.
* @param user - user uploading the file
* @param media - file to upload
* @return - new url from matrix
*/
protected String uploadFileToMatrix(ChatUser user, Media media) {
try {
String fileName = media.getUrl().substring(media.getUrl().lastIndexOf("/") + 1);

// Download file
URL url = new URL(media.getUrl());
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(new LaxRedirectStrategy())
.build();
HttpGet get = new HttpGet(url.toURI());
HttpResponse response = httpclient.execute(get);
InputStream source = response.getEntity().getContent();

return matrixClient.userId(user.getOwnerId()).content()
.upload(source, fileName, media.getContentType())
.join();
} catch (IOException | URISyntaxException | CancellationException | CompletionException error) {
log.error(
String.format("Error uploading file to matrix homeserver (%s): ", getClass()),
error);
// Fallback to initial url, so matrix message is sent instead of just a twilio error.
return media.getUrl();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ca.levimiller.smsbridge.service.impl.matrix.attachment;

import ca.levimiller.smsbridge.data.model.ChatUser;
import ca.levimiller.smsbridge.data.model.Media;
import io.github.ma1uta.matrix.client.AppServiceClient;
import io.github.ma1uta.matrix.event.content.EventContent;
import io.github.ma1uta.matrix.event.message.Audio;
import io.github.ma1uta.matrix.event.nested.AudioInfo;
import javax.inject.Inject;
import org.springframework.stereotype.Service;

@Service
public class AudioAttachmentService extends AbstractAttachmentService {

@Inject
public AudioAttachmentService(AppServiceClient matrixClient) {
super(matrixClient);
}

@Override
protected EventContent getContent(ChatUser user, Media attachment) {
Audio audio = new Audio();
audio.setBody("audio attachment");
audio.setUrl(uploadFileToMatrix(user, attachment));

AudioInfo info = new AudioInfo();
info.setMimetype(attachment.getContentType());
audio.setInfo(info);
return audio;
}

@Override
public boolean supportsType(String contentType) {
return contentType.startsWith("audio");
}
}
Loading

0 comments on commit 9f56731

Please sign in to comment.