-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' of https://github.com/prostasia/rocketchatcsam
- Loading branch information
Showing
18 changed files
with
923 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# These are supported funding model platforms | ||
|
||
github: prostasia # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] | ||
patreon: # Replace with a single Patreon username | ||
open_collective: # Replace with a single Open Collective username | ||
ko_fi: # Replace with a single Ko-fi username | ||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel | ||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry | ||
liberapay: # Replace with a single Liberapay username | ||
issuehunt: # Replace with a single IssueHunt username | ||
otechie: # Replace with a single Otechie username | ||
custom: https://prostasia.org/donations/give/ # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2020 prostasia | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# ignore modules pulled in from npm | ||
node_modules/ | ||
|
||
# rc-apps package output | ||
dist/ | ||
|
||
# JetBrains IDEs | ||
out/ | ||
.idea/ | ||
.idea_modules/ | ||
|
||
# macOS | ||
.DS_Store | ||
.AppleDouble | ||
.LSOverride | ||
._* | ||
.DocumentRevisions-V100 | ||
.fseventsd | ||
.Spotlight-V100 | ||
.TemporaryItems | ||
.Trashes | ||
.VolumeIcon.icns | ||
.com.apple.timemachine.donotpresent | ||
.AppleDB | ||
.AppleDesktop | ||
Network Trash Folder | ||
Temporary Items | ||
.apdisk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { | ||
IAppAccessors, | ||
ILogger, | ||
IConfigurationExtend, | ||
IEnvironmentRead, | ||
IRead, | ||
IHttp, | ||
IPersistence, | ||
IMessageBuilder, | ||
IConfigurationModify, | ||
} from '@rocket.chat/apps-engine/definition/accessors'; | ||
import { App } from '@rocket.chat/apps-engine/definition/App'; | ||
import { IMessage, IPostMessageSent, IPreMessageSentPrevent, IPreMessageSentModify } from '@rocket.chat/apps-engine/definition/messages'; | ||
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; | ||
import { SettingType, ISetting } from '@rocket.chat/apps-engine/definition/settings'; | ||
|
||
import { PhotoDNACloudService } from './helper/PhotoDNACloudService'; | ||
import { IMatchResult } from './helper/IMatchResult'; | ||
import { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom'; | ||
import { SETTING_PHOTODNA_API_KEY, SETTING_QUARANTINE_CHANNEL, SETTING_LIMIT_ANALYSIS_TO_CHANNELS } from './Settings'; | ||
|
||
export class PhotoDnaCsemScanningApp extends App implements IPreMessageSentModify { | ||
|
||
private photoDnaService: PhotoDNACloudService; | ||
|
||
private quarantineChannel: string; | ||
private watchedRoomsId: Set<string> | undefined; | ||
|
||
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { | ||
super(info, logger, accessors); | ||
this.photoDnaService = new PhotoDNACloudService(); | ||
} | ||
|
||
protected async extendConfiguration(configuration: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise<void> { | ||
await configuration.settings.provideSetting({ | ||
id: SETTING_PHOTODNA_API_KEY, | ||
type: SettingType.STRING, | ||
packageValue: '', | ||
required: true, | ||
public: false, | ||
i18nLabel: 'CSEM_Api_Key_Label', | ||
i18nDescription: 'CSEM_Api_Key_Description', | ||
}); | ||
await configuration.settings.provideSetting({ | ||
id: SETTING_QUARANTINE_CHANNEL, | ||
type: SettingType.STRING, | ||
packageValue: 'csem-quarantine', | ||
required: true, | ||
public: false, | ||
i18nLabel: 'CSEM_Quarantine_Target_Channel_Label', | ||
i18nDescription: 'CSEM_Quarantine_Target_Channel_Description', | ||
}); | ||
await configuration.settings.provideSetting({ | ||
id: SETTING_LIMIT_ANALYSIS_TO_CHANNELS, | ||
type: SettingType.STRING, | ||
packageValue: '', | ||
required: true, | ||
public: false, | ||
i18nLabel: 'CSEM_Limit_Analysis_To_Channels_Csv_Label', | ||
i18nDescription: 'CSEM_Limit_Analysis_To_Channels_Csv_Description', | ||
}); | ||
} | ||
|
||
public async onEnable(environment: IEnvironmentRead, configurationModify: IConfigurationModify): Promise<boolean> { | ||
this.quarantineChannel = await environment.getSettings().getValueById(SETTING_QUARANTINE_CHANNEL); | ||
let limitRoomNamesCsv = await environment.getSettings().getValueById(SETTING_LIMIT_ANALYSIS_TO_CHANNELS) | ||
this.initLimitRoomNamesSet(limitRoomNamesCsv); | ||
return true; | ||
} | ||
|
||
public async onSettingUpdated(setting: ISetting, configurationModify: IConfigurationModify, read: IRead, http: IHttp): Promise<void> { | ||
if (SETTING_QUARANTINE_CHANNEL === setting.id) { | ||
this.quarantineChannel = setting.value; | ||
} else if (SETTING_LIMIT_ANALYSIS_TO_CHANNELS === setting.id) { | ||
await this.initLimitRoomNamesSet(setting.value); | ||
} | ||
} | ||
|
||
private async initLimitRoomNamesSet(limitRoomNamesCsv: string) { | ||
this.watchedRoomsId = undefined; | ||
if (limitRoomNamesCsv && limitRoomNamesCsv.length > 0) { | ||
this.watchedRoomsId = new Set<string>(); | ||
let _csvRoomNames = limitRoomNamesCsv.trim(); | ||
let _csvRoomsArray = _csvRoomNames.split(','); | ||
for (const roomName of _csvRoomsArray) { | ||
const room = await this.getAccessors().reader.getRoomReader().getByName(roomName.toLowerCase()); | ||
if (room) { | ||
this.getLogger().debug(`Watching room \'${roomName}\'`); | ||
this.watchedRoomsId!.add(room.id); | ||
} else { | ||
this.getLogger().warn(`Room not found for name \'${roomName}\'. Not adding to watch list.`); | ||
} | ||
} | ||
} | ||
} | ||
|
||
async checkPreMessageSentModify(message: IMessage, read: IRead, http: IHttp): Promise<boolean> { | ||
if (this.watchedRoomsId && this.watchedRoomsId.size > 0) { | ||
if (!this.watchedRoomsId.has(message.room.id)) { | ||
return false; | ||
} | ||
} | ||
return this.photoDnaService.preMatchMessage(message, this.getLogger()); | ||
} | ||
|
||
async executePreMessageSentModify(message: IMessage, builder: IMessageBuilder, read: IRead, http: IHttp, persistence: IPersistence): Promise<IMessage> { | ||
let result = await this.photoDnaService.matchMessage(message, this.getLogger(), read, http); | ||
if (result && result.IsMatch) { | ||
this.handleMatchingMessage(result, message, read, persistence, builder); | ||
} | ||
return builder.getMessage(); | ||
} | ||
|
||
private async handleMatchingMessage(result: IMatchResult, message: IMessage, read: IRead, persistence: IPersistence, builder: IMessageBuilder): Promise<void> { | ||
this.getLogger().warn('CSEM-MATCH', message.id, message.sender, result); | ||
|
||
if (this.quarantineChannel) { | ||
const targetRoom: IRoom | undefined = await read.getRoomReader().getByName(this.quarantineChannel); | ||
if (targetRoom) { | ||
// we have a target room - move it to this room | ||
// the original user uploading currently does not get notified | ||
builder.setRoom(targetRoom); | ||
} else { | ||
this.getLogger().warn('Defined target Room/Channel does not exist: ' + this.quarantineChannel); | ||
// we have no target room - at least remove the image | ||
builder.removeAttachment(0); | ||
} | ||
} else { | ||
this.getLogger().warn('No target channel for quarantined messages provided'); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const SETTING_PHOTODNA_API_KEY: string = "photodna-api-key"; | ||
export const SETTING_QUARANTINE_CHANNEL: string = "csem-quarantine-target-channel"; | ||
export const SETTING_LIMIT_ANALYSIS_TO_CHANNELS = "limit-analysis-to-channels-csv"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"id": "553209bf-81fc-4210-bebc-8943b40e8fea", | ||
"version": "0.2.2", | ||
"requiredApiVersion": "^1.4.0", | ||
"iconFile": "photodna.png", | ||
"author": { | ||
"name": "Jeremy Malcolm / Marco Descher", | ||
"homepage": "n/a", | ||
"support": "n/a" | ||
}, | ||
"name": "PhotoDNA CSEM-scanning", | ||
"nameSlug": "photodna-csem-scanning", | ||
"classFile": "PhotoDnaCsemScanningApp.ts", | ||
"description": "Validate uploaded images against the Microsoft PhotoDNA CSEM (Child Sexual Exploitation Material) Cloud Service" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface IImageData { | ||
contentType: string; | ||
data: Buffer; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export interface IMatchResult { | ||
Status: IMatchOperationStatus; | ||
TrackingId: string; | ||
ContentId?: string; | ||
IsMatch?: boolean; | ||
MatchDetails?: IMatchDetails; | ||
} | ||
|
||
export interface IMatchOperationStatus { | ||
Code: number; | ||
Description: string; | ||
} | ||
|
||
export interface IMatchDetails { | ||
MatchFlags: Array<IMatchFlag>; | ||
} | ||
|
||
export interface IMatchFlag { | ||
AdvancedInfo?: Array<IAdvancedInfo>; | ||
Source?: string; | ||
Violations?: Array<string>; | ||
} | ||
|
||
export interface IAdvancedInfo { | ||
Key: string; | ||
Value: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { IHttp, ILogger, IRead } from "@rocket.chat/apps-engine/definition/accessors"; | ||
import { IImageData } from "./IImageData"; | ||
import { IMatchResult } from "./IMatchResult"; | ||
import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; | ||
import { SETTING_PHOTODNA_API_KEY } from "../Settings"; | ||
|
||
/** | ||
* Validate a message against the Microsoft PhotoDNA cloud service | ||
* @see https://www.microsoft.com/en-us/photodna | ||
*/ | ||
export class PhotoDNACloudService { | ||
|
||
private readonly Match_Post_Url = 'https://api.microsoftmoderator.com/photodna/v1.0/Match'; | ||
|
||
/** | ||
* Determine whether matchMessage is to be executed, which is the case if this message | ||
* contains an image we can handle | ||
* @param message | ||
* @param logger | ||
*/ | ||
async preMatchMessage(message: IMessage, logger: ILogger): Promise<boolean> { | ||
// is there an attachment ? | ||
if (!message.attachments) { | ||
return false; | ||
} | ||
// is it an image ? | ||
if (!message.attachments[0].imageUrl) { | ||
return false; | ||
} | ||
|
||
var imageAttachment: any = message.attachments[0]; | ||
let imageMimeType = imageAttachment.imageType; | ||
// does the PhotoDNA service support this attachment ? | ||
if (!this.isSupportedImageMimeType(imageMimeType)) { | ||
logger.warn('Could not perform match operation on unsupported image type ' + imageMimeType); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Matches the message against the PhotoDNA service. Before executing this method, be sure to call preMatchMessage | ||
* @param message | ||
* @param logger | ||
* @param read | ||
* @param http | ||
*/ | ||
async matchMessage(message: IMessage, logger: ILogger, read: IRead, http: IHttp): Promise<IMatchResult | undefined> { | ||
var imageAttachment: any = message.attachments![0]; | ||
let imageMimeType = imageAttachment.imageType; | ||
// determine image id and load it | ||
let imageId = imageAttachment.imageUrl.substring(0, imageAttachment.imageUrl.lastIndexOf('/')).replace('/file-upload/', '') | ||
// TODO better way to find image id? | ||
let imageBuffer = await read.getUploadReader().getBufferById(imageId) | ||
if (!imageBuffer) { | ||
logger.warn('Could not load image buffer for image id ' + imageId); | ||
return undefined; | ||
} | ||
|
||
let result = await this.performMatchOperation(http, read, { | ||
contentType: imageMimeType, | ||
data: imageBuffer | ||
}); | ||
return result; | ||
|
||
} | ||
|
||
/** | ||
* Perform the match operation as defined by the PhotoDNA cloud service api | ||
* @param http | ||
* @param read | ||
* @param imageData | ||
* @see https://developer.microsoftmoderator.com/docs/services/57c7426e2703740ec4c9f4c3/operations/57c7426f27037407c8cc69e6 | ||
*/ | ||
private async performMatchOperation(http: IHttp, read: IRead, imageData: IImageData): Promise<IMatchResult | undefined> { | ||
const apiKey = await read.getEnvironmentReader().getSettings().getValueById(SETTING_PHOTODNA_API_KEY); | ||
if (apiKey) { | ||
let content = JSON.stringify({ | ||
"DataRepresentation": "inline", | ||
"Value": imageData.data.toString('base64') | ||
}) | ||
|
||
let result = await http.post(this.Match_Post_Url, { | ||
content, | ||
params: { | ||
'enhance': 'false' | ||
}, | ||
headers: { | ||
'Ocp-Apim-Subscription-Key': apiKey | ||
} | ||
}) | ||
if (result.data) { | ||
return result.data as IMatchResult; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
isSupportedImageMimeType(mimeType: string): Boolean { | ||
switch (mimeType) { | ||
case ('image/gif'): | ||
case ('image/jpeg'): | ||
case ('image/png'): | ||
case ('image/bmp'): | ||
case ('image/tiff'): | ||
return true; | ||
} | ||
return false; | ||
} | ||
} |
Oops, something went wrong.