89 Commits
41 changed files with 2439 additions and 118 deletions
-
23.dockerignore
-
20.gitignore
-
5.helm/Chart.yaml
-
21.helm/templates/NOTES.txt
-
56.helm/templates/_helpers.tpl
-
68.helm/templates/deployment.yaml
-
41.helm/templates/ingress.yaml
-
23.helm/templates/persistentVolumeClaim.yaml
-
16.helm/templates/service.yaml
-
8.helm/templates/serviceaccount.yaml
-
15.helm/templates/tests/test-connection.yaml
-
79.helm/values.yaml
-
26Dockerfile
-
28README.md
-
4data/giphy-config.json.example
-
9data/szurubooru-config.json.example
-
20entrypoint.sh
-
54index.js
-
442package-lock.json
-
16package.json
-
266pipeline.yml
-
2scripts/get_build.sh
-
2scripts/get_tag.sh
-
2scripts/get_version.sh
-
27scripts/run_development_docker.sh
-
21scripts/run_development_local.sh
-
195src/bot.ts
-
29src/config.ts
-
172src/engine.ts
-
55src/logging.ts
-
28src/message.ts
-
260src/module/abstract.ts
-
65src/module/admin.ts
-
39src/module/example.ts
-
50src/module/giphy.ts
-
52src/module/help.ts
-
19src/module/index.ts
-
135src/module/szurubooru/client.ts
-
65src/module/szurubooru/index.ts
-
85src/utility.ts
-
14tsconfig.json
@ -0,0 +1,23 @@ |
|||||
|
# Installed node files |
||||
|
node_modules/ |
||||
|
|
||||
|
# Compiled Files |
||||
|
dist/ |
||||
|
|
||||
|
# Logging files |
||||
|
log/ |
||||
|
|
||||
|
# Files open as swp in an editor |
||||
|
*.swp |
||||
|
|
||||
|
# Development / CICD / Deployment files |
||||
|
pipeline.yml |
||||
|
.helm/ |
||||
|
Dockerfile |
||||
|
|
||||
|
# Development documentation |
||||
|
README.md |
||||
|
CONTRIBUTING.md |
||||
|
|
||||
|
# Testing |
||||
|
test/ |
@ -1,8 +1,24 @@ |
|||||
# Node Files |
# Node Files |
||||
node_modules/ |
node_modules/ |
||||
|
|
||||
|
# Compiled Files |
||||
|
dist/ |
||||
|
|
||||
# Config files |
# Config files |
||||
data/config.json |
|
||||
|
data/*config.json |
||||
|
|
||||
|
# Log files |
||||
|
log/ |
||||
|
*.log |
||||
|
|
||||
# Mac Files |
# Mac Files |
||||
.DS_Store |
|
||||
|
.DS_Store |
||||
|
|
||||
|
# Swap files |
||||
|
*.swp |
||||
|
|
||||
|
# Version files |
||||
|
build.info |
||||
|
version.info |
||||
|
tag.info |
||||
|
tag |
@ -0,0 +1,5 @@ |
|||||
|
apiVersion: v1 |
||||
|
appVersion: "1.0" |
||||
|
description: A Helm chart for Kubernetes |
||||
|
name: baphomet-js |
||||
|
version: 0.1.2 |
@ -0,0 +1,21 @@ |
|||||
|
1. Get the application URL by running these commands: |
||||
|
{{- if .Values.ingress.enabled }} |
||||
|
{{- range $host := .Values.ingress.hosts }} |
||||
|
{{- range .paths }} |
||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} |
||||
|
{{- end }} |
||||
|
{{- end }} |
||||
|
{{- else if contains "NodePort" .Values.service.type }} |
||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "baphomet-js.fullname" . }}) |
||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") |
||||
|
echo http://$NODE_IP:$NODE_PORT |
||||
|
{{- else if contains "LoadBalancer" .Values.service.type }} |
||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available. |
||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "baphomet-js.fullname" . }}' |
||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "baphomet-js.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") |
||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }} |
||||
|
{{- else if contains "ClusterIP" .Values.service.type }} |
||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "baphomet-js.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") |
||||
|
echo "Visit http://127.0.0.1:8080 to use your application" |
||||
|
kubectl port-forward $POD_NAME 8080:80 |
||||
|
{{- end }} |
@ -0,0 +1,56 @@ |
|||||
|
{{/* vim: set filetype=mustache: */}} |
||||
|
{{/* |
||||
|
Expand the name of the chart. |
||||
|
*/}} |
||||
|
{{- define "baphomet-js.name" -}} |
||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} |
||||
|
{{- end -}} |
||||
|
|
||||
|
{{/* |
||||
|
Create a default fully qualified app name. |
||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). |
||||
|
If release name contains chart name it will be used as a full name. |
||||
|
*/}} |
||||
|
{{- define "baphomet-js.fullname" -}} |
||||
|
{{- if .Values.fullnameOverride -}} |
||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} |
||||
|
{{- else -}} |
||||
|
{{- $name := default .Chart.Name .Values.nameOverride -}} |
||||
|
{{- if contains $name .Release.Name -}} |
||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" -}} |
||||
|
{{- else -}} |
||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} |
||||
|
{{- end -}} |
||||
|
{{- end -}} |
||||
|
{{- end -}} |
||||
|
|
||||
|
{{/* |
||||
|
Create chart name and version as used by the chart label. |
||||
|
*/}} |
||||
|
{{- define "baphomet-js.chart" -}} |
||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} |
||||
|
{{- end -}} |
||||
|
|
||||
|
{{/* |
||||
|
Common labels |
||||
|
*/}} |
||||
|
{{- define "baphomet-js.labels" -}} |
||||
|
app.kubernetes.io/name: {{ include "baphomet-js.name" . }} |
||||
|
helm.sh/chart: {{ include "baphomet-js.chart" . }} |
||||
|
app.kubernetes.io/instance: {{ .Release.Name }} |
||||
|
{{- if .Chart.AppVersion }} |
||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} |
||||
|
{{- end }} |
||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }} |
||||
|
{{- end -}} |
||||
|
|
||||
|
{{/* |
||||
|
Create the name of the service account to use |
||||
|
*/}} |
||||
|
{{- define "baphomet-js.serviceAccountName" -}} |
||||
|
{{- if .Values.serviceAccount.create -}} |
||||
|
{{ default (include "baphomet-js.fullname" .) .Values.serviceAccount.name }} |
||||
|
{{- else -}} |
||||
|
{{ default "default" .Values.serviceAccount.name }} |
||||
|
{{- end -}} |
||||
|
{{- end -}} |
@ -0,0 +1,68 @@ |
|||||
|
apiVersion: apps/v1 |
||||
|
kind: Deployment |
||||
|
metadata: |
||||
|
name: {{ include "baphomet-js.fullname" . }} |
||||
|
labels: |
||||
|
{{ include "baphomet-js.labels" . | indent 4 }} |
||||
|
spec: |
||||
|
replicas: {{ .Values.replicaCount }} |
||||
|
selector: |
||||
|
matchLabels: |
||||
|
app.kubernetes.io/name: {{ include "baphomet-js.name" . }} |
||||
|
app.kubernetes.io/instance: {{ .Release.Name }} |
||||
|
template: |
||||
|
metadata: |
||||
|
labels: |
||||
|
app.kubernetes.io/name: {{ include "baphomet-js.name" . }} |
||||
|
app.kubernetes.io/instance: {{ .Release.Name }} |
||||
|
spec: |
||||
|
{{- with .Values.imagePullSecrets }} |
||||
|
imagePullSecrets: |
||||
|
{{- toYaml . | nindent 8 }} |
||||
|
{{- end }} |
||||
|
serviceAccountName: {{ template "baphomet-js.serviceAccountName" . }} |
||||
|
securityContext: |
||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }} |
||||
|
containers: |
||||
|
- name: {{ .Chart.Name }} |
||||
|
securityContext: |
||||
|
{{- toYaml .Values.securityContext | nindent 12 }} |
||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" |
||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }} |
||||
|
env: |
||||
|
- name: LOG_LEVEL |
||||
|
value: {{ .Values.app.env.log_level }} |
||||
|
- name: NODE_ENV |
||||
|
value: {{ .Values.app.env.node_env }} |
||||
|
resources: |
||||
|
{{- toYaml .Values.resources | nindent 12 }} |
||||
|
volumeMounts: |
||||
|
- mountPath: /opt/baphomet/data |
||||
|
name: data |
||||
|
subPath: data |
||||
|
- mountPath: /opt/baphomet/log |
||||
|
name: data |
||||
|
subPath: log |
||||
|
volumes: |
||||
|
- name: config |
||||
|
configMap: |
||||
|
name: {{ include "baphomet-js.fullname" . }} |
||||
|
- name: data |
||||
|
{{- if .Values.persistence.enabled }} |
||||
|
persistentVolumeClaim: |
||||
|
claimName: {{ .Values.persistence.existingClaim | default (include "baphomet-js.fullname" .) }} |
||||
|
{{- else }} |
||||
|
emptyDir: {} |
||||
|
{{- end -}} |
||||
|
{{- with .Values.nodeSelector }} |
||||
|
nodeSelector: |
||||
|
{{- toYaml . | nindent 8 }} |
||||
|
{{- end }} |
||||
|
{{- with .Values.affinity }} |
||||
|
affinity: |
||||
|
{{- toYaml . | nindent 8 }} |
||||
|
{{- end }} |
||||
|
{{- with .Values.tolerations }} |
||||
|
tolerations: |
||||
|
{{- toYaml . | nindent 8 }} |
||||
|
{{- end }} |
@ -0,0 +1,41 @@ |
|||||
|
{{- if .Values.ingress.enabled -}} |
||||
|
{{- $fullName := include "baphomet-js.fullname" . -}} |
||||
|
{{- $svcPort := .Values.service.port -}} |
||||
|
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} |
||||
|
apiVersion: networking.k8s.io/v1beta1 |
||||
|
{{- else -}} |
||||
|
apiVersion: extensions/v1beta1 |
||||
|
{{- end }} |
||||
|
kind: Ingress |
||||
|
metadata: |
||||
|
name: {{ $fullName }} |
||||
|
labels: |
||||
|
{{ include "baphomet-js.labels" . | indent 4 }} |
||||
|
{{- with .Values.ingress.annotations }} |
||||
|
annotations: |
||||
|
{{- toYaml . | nindent 4 }} |
||||
|
{{- end }} |
||||
|
spec: |
||||
|
{{- if .Values.ingress.tls }} |
||||
|
tls: |
||||
|
{{- range .Values.ingress.tls }} |
||||
|
- hosts: |
||||
|
{{- range .hosts }} |
||||
|
- {{ . | quote }} |
||||
|
{{- end }} |
||||
|
secretName: {{ .secretName }} |
||||
|
{{- end }} |
||||
|
{{- end }} |
||||
|
rules: |
||||
|
{{- range .Values.ingress.hosts }} |
||||
|
- host: {{ .host | quote }} |
||||
|
http: |
||||
|
paths: |
||||
|
{{- range .paths }} |
||||
|
- path: {{ . }} |
||||
|
backend: |
||||
|
serviceName: {{ $fullName }} |
||||
|
servicePort: {{ $svcPort }} |
||||
|
{{- end }} |
||||
|
{{- end }} |
||||
|
{{- end }} |
@ -0,0 +1,23 @@ |
|||||
|
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} |
||||
|
kind: PersistentVolumeClaim |
||||
|
apiVersion: v1 |
||||
|
metadata: |
||||
|
name: {{ include "baphomet-js.fullname" . }} |
||||
|
labels: |
||||
|
app: {{ include "baphomet-js.fullname" . }} |
||||
|
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" |
||||
|
release: "{{ .Release.Name }}" |
||||
|
heritage: "{{ .Release.Service }}" |
||||
|
annotations: |
||||
|
{{- if .Values.persistence.storageClass }} |
||||
|
volume.beta.kubernetes.io/storage-class: {{ .Values.persistence.storageClass | quote }} |
||||
|
{{- else }} |
||||
|
volume.alpha.kubernetes.io/storage-class: default |
||||
|
{{- end }} |
||||
|
spec: |
||||
|
accessModes: |
||||
|
- {{ .Values.persistence.accessMode | quote }} |
||||
|
resources: |
||||
|
requests: |
||||
|
storage: {{ .Values.persistence.size | quote }} |
||||
|
{{- end }} |
@ -0,0 +1,16 @@ |
|||||
|
apiVersion: v1 |
||||
|
kind: Service |
||||
|
metadata: |
||||
|
name: {{ include "baphomet-js.fullname" . }} |
||||
|
labels: |
||||
|
{{ include "baphomet-js.labels" . | indent 4 }} |
||||
|
spec: |
||||
|
type: {{ .Values.service.type }} |
||||
|
ports: |
||||
|
- port: {{ .Values.service.port }} |
||||
|
targetPort: http |
||||
|
protocol: TCP |
||||
|
name: http |
||||
|
selector: |
||||
|
app.kubernetes.io/name: {{ include "baphomet-js.name" . }} |
||||
|
app.kubernetes.io/instance: {{ .Release.Name }} |
@ -0,0 +1,8 @@ |
|||||
|
{{- if .Values.serviceAccount.create -}} |
||||
|
apiVersion: v1 |
||||
|
kind: ServiceAccount |
||||
|
metadata: |
||||
|
name: {{ template "baphomet-js.serviceAccountName" . }} |
||||
|
labels: |
||||
|
{{ include "baphomet-js.labels" . | indent 4 }} |
||||
|
{{- end -}} |
@ -0,0 +1,15 @@ |
|||||
|
apiVersion: v1 |
||||
|
kind: Pod |
||||
|
metadata: |
||||
|
name: "{{ include "baphomet-js.fullname" . }}-test-connection" |
||||
|
labels: |
||||
|
{{ include "baphomet-js.labels" . | indent 4 }} |
||||
|
annotations: |
||||
|
"helm.sh/hook": test-success |
||||
|
spec: |
||||
|
containers: |
||||
|
- name: wget |
||||
|
image: busybox |
||||
|
command: ['wget'] |
||||
|
args: ['{{ include "baphomet-js.fullname" . }}:{{ .Values.service.port }}'] |
||||
|
restartPolicy: Never |
@ -0,0 +1,79 @@ |
|||||
|
# Default values for baphomet-js. |
||||
|
# This is a YAML-formatted file. |
||||
|
# Declare variables to be passed into your templates. |
||||
|
|
||||
|
replicaCount: 1 |
||||
|
|
||||
|
image: |
||||
|
repository: nexus.nulloctet.com:5000/nulloctet/baphomet-js |
||||
|
tag: stable |
||||
|
pullPolicy: IfNotPresent |
||||
|
|
||||
|
app: |
||||
|
env: |
||||
|
log_level: "info" |
||||
|
node_env: "production" |
||||
|
|
||||
|
persistence: |
||||
|
accessMode: ReadWriteOnce |
||||
|
enabled: true |
||||
|
size: 10Mi |
||||
|
storageClass: storage |
||||
|
|
||||
|
imagePullSecrets: [] |
||||
|
nameOverride: "" |
||||
|
fullnameOverride: "" |
||||
|
|
||||
|
serviceAccount: |
||||
|
# Specifies whether a service account should be created |
||||
|
create: true |
||||
|
# The name of the service account to use. |
||||
|
# If not set and create is true, a name is generated using the fullname template |
||||
|
name: |
||||
|
|
||||
|
podSecurityContext: {} |
||||
|
# fsGroup: 2000 |
||||
|
|
||||
|
securityContext: {} |
||||
|
# capabilities: |
||||
|
# drop: |
||||
|
# - ALL |
||||
|
# readOnlyRootFilesystem: true |
||||
|
# runAsNonRoot: true |
||||
|
# runAsUser: 1000 |
||||
|
|
||||
|
service: |
||||
|
type: ClusterIP |
||||
|
port: 80 |
||||
|
|
||||
|
ingress: |
||||
|
enabled: false |
||||
|
annotations: {} |
||||
|
# kubernetes.io/ingress.class: nginx |
||||
|
# kubernetes.io/tls-acme: "true" |
||||
|
hosts: |
||||
|
- host: chart-example.local |
||||
|
paths: [] |
||||
|
|
||||
|
tls: [] |
||||
|
# - secretName: chart-example-tls |
||||
|
# hosts: |
||||
|
# - chart-example.local |
||||
|
|
||||
|
resources: {} |
||||
|
# We usually recommend not to specify default resources and to leave this as a conscious |
||||
|
# choice for the user. This also increases chances charts run on environments with little |
||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following |
||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'. |
||||
|
# limits: |
||||
|
# cpu: 100m |
||||
|
# memory: 128Mi |
||||
|
# requests: |
||||
|
# cpu: 100m |
||||
|
# memory: 128Mi |
||||
|
|
||||
|
nodeSelector: {} |
||||
|
|
||||
|
tolerations: [] |
||||
|
|
||||
|
affinity: {} |
@ -0,0 +1,26 @@ |
|||||
|
FROM node:12.14-stretch AS builder |
||||
|
WORKDIR /opt/baphomet |
||||
|
COPY . /opt/baphomet |
||||
|
|
||||
|
RUN cd /opt/baphomet \ |
||||
|
&& npm install \ |
||||
|
&& npm run build |
||||
|
|
||||
|
FROM node:12.14-stretch |
||||
|
WORKDIR /opt/baphomet |
||||
|
COPY . /opt/baphomet |
||||
|
|
||||
|
ENV NODE_ENV=production |
||||
|
ENV LOG_LEVEL=warn |
||||
|
|
||||
|
RUN rm -rf /opt/baphomet/src \ |
||||
|
&& mkdir /opt/baphomet/log |
||||
|
|
||||
|
COPY --from=builder /opt/baphomet/dist /opt/baphomet/dist |
||||
|
|
||||
|
RUN cd /opt/baphomet \ |
||||
|
&& npm install --only=prod \ |
||||
|
&& chmod +x entrypoint.sh |
||||
|
|
||||
|
ENTRYPOINT [ "./entrypoint.sh" ] |
||||
|
CMD [ "run" ] |
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"endpoint": "api.giphy.com/v1", |
||||
|
"apiKey": "<Your API Key Here>" |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"url": "<api endpoint>", |
||||
|
"username": "<connecting username>", |
||||
|
"token": "<connection api token>", |
||||
|
"random_image_cache": {}, |
||||
|
"upload_rooms": { |
||||
|
"<authorized room to upload from>" |
||||
|
} |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
#! /usr/bin/env sh |
||||
|
|
||||
|
DIR="$( cd "$( dirname "${0}" )" >/dev/null 2>&1 && pwd )" |
||||
|
export NODE_PATH="${DIR}" |
||||
|
|
||||
|
echo "NODE_ENV: ${NODE_ENV}" |
||||
|
echo "NODE_PATH: ${NODE_PATH}" |
||||
|
echo "LOG_LEVEL: ${LOG_LEVEL}" |
||||
|
|
||||
|
echo "Running build version: $(cat build.info)" |
||||
|
|
||||
|
case $1 in |
||||
|
run) |
||||
|
echo "Starting Baphomet..." |
||||
|
exec node index.js |
||||
|
;; |
||||
|
*) |
||||
|
echo "\"$1\" is an unrecognized command" |
||||
|
;; |
||||
|
esac |
@ -1,48 +1,10 @@ |
|||||
let sdk = require("matrix-js-sdk"); |
|
||||
let config = require("./data/config.json"); |
|
||||
|
let bot = require('./dist/bot'); |
||||
|
let { getConfig } = require('./dist/config'); |
||||
|
let engine = require('./dist/engine'); |
||||
|
|
||||
console.log("Running with config:"); |
|
||||
console.log(config); |
|
||||
|
let config = getConfig(process.env.NODE_PATH + "/data/config.json", ['accessToken']) |
||||
|
|
||||
let client = sdk.createClient({ |
|
||||
baseUrl: config.baseUrl, |
|
||||
accessToken: config.accessToken, |
|
||||
userId: config.userId |
|
||||
}); |
|
||||
|
|
||||
function createBasicTextMessage(body) { |
|
||||
return { |
|
||||
"body": body, |
|
||||
"msgtype": "m.text" |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function sendClientStatusUpdate() { |
|
||||
config.statusRooms.forEach(roomId => { |
|
||||
console.log("Notifying %s", roomId); |
|
||||
client.sendMessage(roomId, createBasicTextMessage("Started!")).done(function() { |
|
||||
console.log("Notified %s", roomId); |
|
||||
}) |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// Prep the bot
|
|
||||
client.on("sync", function (state, previousState, data) { |
|
||||
switch (state) { |
|
||||
case "PREPARED": |
|
||||
sendClientStatusUpdate(); |
|
||||
break; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// auto join rooms that an admin user has invited the bot to
|
|
||||
client.on("RoomMember.membership", function (event, member) { |
|
||||
if (member.membership === "invite" |
|
||||
&& config.admin.indexOf(ember.userId) >= 0) { |
|
||||
client.joinRoom(member.roomId).done(function () { |
|
||||
console.log("Auto-joined %s", member.roomId); |
|
||||
}); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
client.startClient() |
|
||||
|
engine.create( |
||||
|
config, |
||||
|
bot.create(config) |
||||
|
).init().run(); |
@ -0,0 +1,2 @@ |
|||||
|
#!/usr/bin/env sh |
||||
|
echo "$(git describe --tags --long)_$(date --rfc-3339=seconds | sed 's/ /T/g')" |
@ -0,0 +1,2 @@ |
|||||
|
#!/usr/bin/env sh |
||||
|
echo "$(git describe --tags | awk '{split($0,a,"-"); print a[1]}')" |
@ -0,0 +1,2 @@ |
|||||
|
#!/usr/bin/env sh |
||||
|
echo "$(git describe --tags --long)" |
@ -0,0 +1,27 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
|
||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" |
||||
|
pushd "${DIR}/.." |
||||
|
|
||||
|
echo "Writing local testing build.info" |
||||
|
./scripts/get_build.sh > build.info |
||||
|
echo "DOCKER_DEVELOPMENT_"$(cat build.info) > build.info |
||||
|
|
||||
|
CONTAINER_NAME=baphomet-dev |
||||
|
IMAGE_NAME=baphomet-js |
||||
|
IMAGE_TAG=dev |
||||
|
IMAGE_BUILD_DIR=. |
||||
|
|
||||
|
docker build -t ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_BUILD_DIR} |
||||
|
|
||||
|
rm build.info |
||||
|
|
||||
|
docker rm ${CONTAINER_NAME} |
||||
|
|
||||
|
docker run -it \ |
||||
|
-e NODE_ENV=development \ |
||||
|
-e LOG_LEVEL=debug \ |
||||
|
--name ${CONTAINER_NAME} \ |
||||
|
${IMAGE_NAME}:${IMAGE_TAG} |
||||
|
|
||||
|
popd |
@ -0,0 +1,21 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
|
||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" |
||||
|
pushd "${DIR}/.." |
||||
|
|
||||
|
echo "Writing local testing build.info" |
||||
|
./scripts/get_build.sh > build.info |
||||
|
echo "LOCAL_DEVELOPMENT_"$(cat build.info) > build.info |
||||
|
|
||||
|
export NODE_ENV=development |
||||
|
export LOG_LEVEL=debug |
||||
|
|
||||
|
set -e |
||||
|
npm run build |
||||
|
set +e |
||||
|
|
||||
|
./entrypoint.sh run |
||||
|
|
||||
|
rm build.info |
||||
|
|
||||
|
popd |
@ -0,0 +1,195 @@ |
|||||
|
let fs = require('fs'); |
||||
|
let sdk = require('matrix-js-sdk'); |
||||
|
let message = require('./message'); |
||||
|
let utility = require('./utility'); |
||||
|
let { logger } = require('./logging'); |
||||
|
|
||||
|
class Bot { |
||||
|
|
||||
|
client: any; |
||||
|
config: any; |
||||
|
buildInfo: string; |
||||
|
connected: boolean; |
||||
|
startTime: Date; |
||||
|
|
||||
|
constructor(config, buildInfo) { |
||||
|
this.config = config; |
||||
|
this.buildInfo = buildInfo; |
||||
|
this.connected = false; |
||||
|
this.startTime = new Date(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initialize the bot connection |
||||
|
*/ |
||||
|
init(messageCallback: any) { |
||||
|
logger.info("Creating Matrix Client") |
||||
|
this.client = sdk.createClient({ |
||||
|
baseUrl: this.config.baseUrl, |
||||
|
userId: this.config.userId |
||||
|
}); |
||||
|
|
||||
|
this.client.on("sync", async (state: any, previousState: any, data: any) => { |
||||
|
switch (state) { |
||||
|
case "PREPARED": |
||||
|
this.connected = true; |
||||
|
await this.sendStatusStartup(); |
||||
|
await this.updateAvatar(process.env.NODE_PATH + '/assets/avatar.jpg'); |
||||
|
this.client.getJoinedRooms() |
||||
|
.done((rooms) => { |
||||
|
logger.info("Connected to: %o", rooms) |
||||
|
}); |
||||
|
break; |
||||
|
case "SYNCING": |
||||
|
logger.debug("Syncing") |
||||
|
break; |
||||
|
case "RECONNECTING": |
||||
|
logger.debug("Reconnecting"); |
||||
|
break; |
||||
|
default: |
||||
|
logger.error("Unexpected sync state: %s", state); |
||||
|
process.exit(1); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.client.on("RoomMember.membership", (event: any, member: any) => { |
||||
|
if (member.membership === "invite" |
||||
|
&& this.config.admin.indexOf(member.userId) >= 0) { |
||||
|
this.client.joinRoom(member.roomId).done(() => { |
||||
|
logger.info("Auto-joined %s", member.roomId); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.client.on("Room.timeline", messageCallback); |
||||
|
|
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
async connect() { |
||||
|
// logger.info("Initializing Crypto");
|
||||
|
// await bot.client.initCrypto();
|
||||
|
let botClient = this.client; |
||||
|
let botConfig = this.config; |
||||
|
|
||||
|
let startServerConnection = () => { |
||||
|
logger.info("Starting Matrix SDK Client"); |
||||
|
return this.client.startClient({ |
||||
|
initialSyncLimit: 0 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
let attemptPasswordLogin = (botClient, botConfig) => { |
||||
|
return botClient.loginWithPassword(botConfig.userId, botConfig.userPassword) |
||||
|
.then((data) => { |
||||
|
logger.info("Successfully authenticated with password %o", data); |
||||
|
return startServerConnection(); |
||||
|
}, (err) => { |
||||
|
logger.error("Failed to authenticate with password %o", err); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
logger.info("Authenticating With Server"); |
||||
|
if (typeof botConfig.accessToken !== 'undefined') { |
||||
|
await botClient.loginWithToken(this.config.accessToken) |
||||
|
.then((data) => { |
||||
|
logger.info("Successfully authenticated with access token %o", data); |
||||
|
return startServerConnection(); |
||||
|
}, (err) => { |
||||
|
logger.error("Failed to authenticate with access token %o", err); |
||||
|
if (typeof botConfig.userPassword !== 'undefined') { |
||||
|
return attemptPasswordLogin(botClient, botConfig); |
||||
|
} else { |
||||
|
logger.error("No fallback password provided!"); |
||||
|
} |
||||
|
}); |
||||
|
} else if (typeof botConfig.userPassword !== 'undefined') { |
||||
|
await attemptPasswordLogin(botClient, botConfig); |
||||
|
} else { |
||||
|
logger.error("No authentication credentials available!"); |
||||
|
process.exit(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateAvatar(avatarFile, overwrite = false) { |
||||
|
let matrixClient = this.client; |
||||
|
let botConfig = this.config; |
||||
|
let promises = [Promise.resolve(true)]; |
||||
|
if (this.connected) { |
||||
|
promises.push(this.client.getProfileInfo(this.config.userId, "avatar_url") |
||||
|
.then((existingAvatarUrl) => { |
||||
|
logger.info("Recieved avatar_url: %o", existingAvatarUrl); |
||||
|
if (typeof existingAvatarUrl !== 'undefined' && typeof existingAvatarUrl == 'object' |
||||
|
&& existingAvatarUrl.constructor === Object && Object.keys(existingAvatarUrl).length !== 0 |
||||
|
&& !overwrite) { |
||||
|
logger.info("Avatar already set"); |
||||
|
} else { |
||||
|
logger.info("Setting avatar content from %s", avatarFile); |
||||
|
let avatarFileBuffer = fs.readFileSync(avatarFile); |
||||
|
logger.debug("Avatar Image Data %o", avatarFileBuffer); |
||||
|
matrixClient.uploadContent(avatarFileBuffer, { |
||||
|
name: botConfig.userId + " avatar", |
||||
|
type: "image/jpeg", |
||||
|
rawResponse: false |
||||
|
}).then((uploadedAvatar) => { |
||||
|
logger.info("Uploaded avatar %o", uploadedAvatar); |
||||
|
matrixClient.setAvatarUrl(uploadedAvatar.content_uri) |
||||
|
.then(() => { |
||||
|
logger.info("Updated %s avatar to %s", botConfig.userId, uploadedAvatar.content_uri); |
||||
|
return true; |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
})); |
||||
|
} else { |
||||
|
logger.warn("Attempting to update avatar while disconnected"); |
||||
|
} |
||||
|
return Promise.all(promises); |
||||
|
} |
||||
|
|
||||
|
sendStatusStartup() { |
||||
|
let promises = [Promise.resolve(true)] |
||||
|
if (this.connected) { |
||||
|
this.config.statusRooms.forEach(roomId => { |
||||
|
logger.debug("Notifying %s of startup", roomId); |
||||
|
promises.push(this.client.sendMessage( |
||||
|
roomId, message.createBasic(`Started at ${utility.toISODateString(this.startTime)} with version: ${this.buildInfo}`, message.types.NOTICE) |
||||
|
).then(() => { |
||||
|
logger.debug("Notified %s of startup", roomId); |
||||
|
}, (err) => { |
||||
|
logger.error("Unable to send message to room %s because %s", roomId, err.errcode); |
||||
|
})); |
||||
|
}); |
||||
|
} else { |
||||
|
logger.warn("Attempting to send startup message while disconnected"); |
||||
|
} |
||||
|
return Promise.all(promises); |
||||
|
} |
||||
|
|
||||
|
sendStatusShutdown() { |
||||
|
let promises = [Promise.resolve(true)] |
||||
|
if (this.connected) { |
||||
|
this.config.statusRooms.forEach(roomId => { |
||||
|
logger.debug("Notifying %s of shutdown", roomId); |
||||
|
promises.push(this.client.sendMessage( |
||||
|
roomId, message.createBasic(`Shutting down at ${utility.toISODateString(new Date())} with version: ${this.buildInfo}`, message.types.NOTICE) |
||||
|
).then(() => { |
||||
|
logger.debug("Notified %s of shutdown", roomId); |
||||
|
}, (err) => { |
||||
|
logger.error("Unable to send message to room %s because %s", roomId, err.errcode); |
||||
|
})); |
||||
|
}); |
||||
|
} else { |
||||
|
logger.warn("Attempting to send shutdown message while disconnected"); |
||||
|
} |
||||
|
return Promise.all(promises); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function create(config) { |
||||
|
let buildInfo = utility.getBuildInfo(); |
||||
|
logger.info("Running version: %s", buildInfo); |
||||
|
return new Bot(config, buildInfo); |
||||
|
} |
||||
|
|
||||
|
export { create }; |
@ -0,0 +1,29 @@ |
|||||
|
let fs = require('fs'); |
||||
|
let { logger } = require('./logging'); |
||||
|
|
||||
|
let loadedConfigs = new Map(); |
||||
|
|
||||
|
function sanitizeConfig(config: any, fields = []) : any { |
||||
|
let clonedConfig = { ...config }; |
||||
|
fields.forEach((field) => { |
||||
|
clonedConfig[field] = '******' |
||||
|
}) |
||||
|
return clonedConfig; |
||||
|
} |
||||
|
|
||||
|
function getConfig(configFile: string, sanitizedFields: Array<string> = [], reload: Boolean = false) : any { |
||||
|
if (loadedConfigs.has(configFile) && !reload) { |
||||
|
return loadedConfigs.get(configFile); |
||||
|
} else { |
||||
|
logger.info("Reading config: %s", configFile); |
||||
|
let rawConfigData = fs.readFileSync(configFile); |
||||
|
let config = JSON.parse(rawConfigData); |
||||
|
logger.info("Loaded config: %s", configFile); |
||||
|
logger.debug("%o", sanitizeConfig(config, sanitizedFields)); |
||||
|
loadedConfigs.set(configFile, config); |
||||
|
return config; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { getConfig }; |
||||
|
export { sanitizeConfig }; |
@ -0,0 +1,172 @@ |
|||||
|
import { AbstractModule } from "./module/abstract"; |
||||
|
|
||||
|
let { logger } = require('./logging'); |
||||
|
let { modules } = require('./module/index'); |
||||
|
let trie = require('trie-prefix-tree'); |
||||
|
let { getShortestPrefix } = require('./utility'); |
||||
|
let help = require('./module/help'); |
||||
|
let message = require('./message'); |
||||
|
let utility = require('./utility'); |
||||
|
let { initModule } = require('./module/abstract'); |
||||
|
let { sanitizeConfig } = require('./config'); |
||||
|
|
||||
|
let commandPrefix = '!'; |
||||
|
|
||||
|
class Engine { |
||||
|
|
||||
|
config: any; |
||||
|
bot: any; |
||||
|
modules: Array<AbstractModule>; |
||||
|
moduleMap: Map<string, AbstractModule>; |
||||
|
commands: Array<string>; |
||||
|
commandMap: Map<string, AbstractModule>; |
||||
|
commandRadixTree: any; |
||||
|
helpModule: any; |
||||
|
|
||||
|
constructor(config, bot, modules) { |
||||
|
this.config = config; |
||||
|
this.bot = bot; |
||||
|
this.modules = modules; |
||||
|
this.moduleMap = new Map(); |
||||
|
this.commands = []; |
||||
|
this.commandMap = new Map(); |
||||
|
this.commandRadixTree = trie([]); |
||||
|
} |
||||
|
|
||||
|
initModules() { |
||||
|
let sanitizedGlobalConfig = sanitizeConfig( |
||||
|
this.config, |
||||
|
[ |
||||
|
'userPassword', |
||||
|
'accessToken' |
||||
|
]) |
||||
|
|
||||
|
this.modules.forEach((mod) => { |
||||
|
logger.info("Loading module: %s", mod.name); |
||||
|
initModule(mod, sanitizedGlobalConfig); |
||||
|
logger.info("Recognized commands: %s", mod.getRecognizedCommands()) |
||||
|
this.moduleMap.set(mod.command, mod); |
||||
|
this.commandMap.set(mod.command, mod); |
||||
|
this.commandRadixTree.addWord(mod.command); |
||||
|
}); |
||||
|
|
||||
|
this.modules.forEach((mod) => { |
||||
|
let shortCharCommand = getShortestPrefix(this.commandRadixTree, mod.command, 1); |
||||
|
let short3CharCommand = getShortestPrefix(this.commandRadixTree, mod.command, 3); |
||||
|
let shortCommandAliases = [shortCharCommand, short3CharCommand]; |
||||
|
logger.info("Adding short command %s for module: %s", shortCommandAliases, mod.name); |
||||
|
shortCommandAliases.forEach((commandAlias) => { |
||||
|
this.commandMap.set(commandAlias, mod); |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
this.helpModule = help.create(this.moduleMap) |
||||
|
initModule(this.helpModule); |
||||
|
this.moduleMap.set(this.helpModule.command, this.helpModule); |
||||
|
this.commandMap.set('help', this.helpModule); |
||||
|
this.commands = Array.from(this.commandMap.keys()).sort() |
||||
|
|
||||
|
logger.info("Bound modules to keywords: %o", this.moduleMap); |
||||
|
} |
||||
|
|
||||
|
init() { |
||||
|
logger.info("Initializing modules"); |
||||
|
this.initModules(); |
||||
|
|
||||
|
/* Bind Message Parsing */ |
||||
|
let engine = this; |
||||
|
let handleEvent = (event, room, toStartOfTimeline) => { |
||||
|
/* Don't process messages from self */ |
||||
|
if (event.sender.userId !== this.config.userId) { |
||||
|
/* don't process messages that aren't of type m.room.message */ |
||||
|
if (event.getType() !== "m.room.message") { |
||||
|
logger.debug("Recieved message of type: %s", event.getType()); |
||||
|
return; |
||||
|
} else { |
||||
|
let messageBody = event.event.content.body; |
||||
|
logger.debug("[%s] %s", room.name, messageBody); |
||||
|
if (messageBody.indexOf(commandPrefix) === 0) { |
||||
|
let command = messageBody.split(' ')[0].substring(1); |
||||
|
if (engine.commandMap.has(command)) { |
||||
|
engine.commandMap.get(command).handleMessage(event, room, sendResponseMessageCallback(engine.bot)); |
||||
|
} else { |
||||
|
let responseMessage = "The following commands are recognized" |
||||
|
responseMessage += "\n" + engine.commands.join(", ") |
||||
|
responseMessage += "\nAdditional information can be discovered with !help <command>" |
||||
|
sendResponseMessage(engine.bot, room, responseMessage); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.bot.init(handleEvent); |
||||
|
|
||||
|
/* Capture Exit Conditions */ |
||||
|
|
||||
|
let signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM"]; |
||||
|
signals.forEach((signature: NodeJS.Signals) => { |
||||
|
process.on(signature, async () => { |
||||
|
await this.bot.sendStatusShutdown() |
||||
|
.then(() => { |
||||
|
logger.info("Gracefully stopping Matrix SDK Client") |
||||
|
this.bot.client.stopClient(); |
||||
|
}); |
||||
|
process.exit(0); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
process.on('exit', () => { |
||||
|
logger.info("Shutting Down"); |
||||
|
}); |
||||
|
|
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
run() { |
||||
|
this.bot.connect(); |
||||
|
return this; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle the callback sending messages via the bot |
||||
|
* |
||||
|
* @param {*} bot |
||||
|
* @param {*} room |
||||
|
* @param {*} responseMessage |
||||
|
*/ |
||||
|
function sendResponseMessage(bot, room, responseMessage) { |
||||
|
logger.debug("Responding to room: %s with %o", room.roomId, responseMessage); |
||||
|
Promise.resolve(responseMessage).then((promisedMessage) => { |
||||
|
if (responseMessage !== null) { |
||||
|
logger.debug("Sending message: %s", promisedMessage); |
||||
|
if (promisedMessage instanceof Object) { |
||||
|
bot.client.sendMessage(room.roomId, promisedMessage); |
||||
|
} else if (utility.isString(promisedMessage)) { |
||||
|
bot.client.sendMessage(room.roomId, message.createBasic(promisedMessage)); |
||||
|
} else { |
||||
|
logger.error("Unable to process response message: %s", promisedMessage); |
||||
|
} |
||||
|
} else { |
||||
|
logger.warn("No response message offered"); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wrapper to produce a callback function that can be passed to the modules |
||||
|
* |
||||
|
* @param {*} bot |
||||
|
*/ |
||||
|
function sendResponseMessageCallback(bot) { |
||||
|
return (room, responseMessage) => { |
||||
|
sendResponseMessage(bot, room, responseMessage); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function create(config, bot) { |
||||
|
return new Engine(config, bot, modules) |
||||
|
} |
||||
|
|
||||
|
export { create }; |
@ -0,0 +1,55 @@ |
|||||
|
let winston = require('winston'); |
||||
|
require('winston-daily-rotate-file'); |
||||
|
|
||||
|
let errorTransport = new (winston.transports.DailyRotateFile)({ |
||||
|
level: 'error', |
||||
|
filename: 'log/error-%DATE%.log', |
||||
|
datePattern: 'YYYY-MM-DD', |
||||
|
zippedArchive: true, |
||||
|
maxFiles: '7d' |
||||
|
}); |
||||
|
|
||||
|
let combinedTransport = new (winston.transports.DailyRotateFile)({ |
||||
|
filename: 'log/combined-%DATE%.log', |
||||
|
datePattern: 'YYYY-MM-DD', |
||||
|
zippedArchive: true, |
||||
|
maxFiles: '7d' |
||||
|
}); |
||||
|
|
||||
|
let logger = winston.createLogger({ |
||||
|
level: 'info', |
||||
|
format: winston.format.combine( |
||||
|
winston.format.timestamp(), |
||||
|
winston.format.splat(), |
||||
|
winston.format.simple() |
||||
|
), |
||||
|
defaultMeta: { service: 'baphomet-js' }, |
||||
|
transports: [ |
||||
|
errorTransport, |
||||
|
combinedTransport |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
if (process.env.NODE_ENV !== 'production') { |
||||
|
logger.add(new winston.transports.Console({ |
||||
|
format: winston.format.combine( |
||||
|
winston.format.colorize(), |
||||
|
winston.format.simple() |
||||
|
) |
||||
|
})); |
||||
|
} else { |
||||
|
logger.add(new winston.transports.Console({ |
||||
|
level: 'error', |
||||
|
format: winston.format.combine( |
||||
|
winston.format.colorize(), |
||||
|
winston.format.simple() |
||||
|
) |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
if ('LOG_LEVEL' in process.env) { |
||||
|
logger.info('LOG_LEVEL: %s', process.env.LOG_LEVEL) |
||||
|
logger.level = process.env.LOG_LEVEL |
||||
|
} |
||||
|
|
||||
|
export { logger }; |
@ -0,0 +1,28 @@ |
|||||
|
let showdown = require('showdown'); |
||||
|
|
||||
|
let converter = new showdown.Converter(); |
||||
|
|
||||
|
enum MessageTypes { |
||||
|
TEXT = 'm.text', |
||||
|
NOTICE = 'm.notice' |
||||
|
} |
||||
|
|
||||
|
function createBasicMessage(body: string, msgtype: MessageTypes = MessageTypes.TEXT) { |
||||
|
return { |
||||
|
"body": body, |
||||
|
"msgtype": msgtype |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function createMarkdownMessage(body: string, markdown: string, msgtype: MessageTypes = MessageTypes.TEXT) { |
||||
|
return { |
||||
|
"body": body, |
||||
|
"msgtype": msgtype, |
||||
|
"format": "org.matrix.custom.html", |
||||
|
"formatted_body": converter.makeHtml(markdown) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export { MessageTypes as types }; |
||||
|
export { createBasicMessage as createBasic }; |
||||
|
export { createMarkdownMessage as createMarkdown }; |
@ -0,0 +1,260 @@ |
|||||
|
/** |
||||
|
* Base module that all modules extend |
||||
|
*/ |
||||
|
|
||||
|
let { logger } = require('../logging'); |
||||
|
let message = require('../message'); |
||||
|
let { isFunction, getObjectKeysToPrototype } = require('../utility'); |
||||
|
let { getConfig, sanitizeConfig } = require('../config'); |
||||
|
|
||||
|
class AbstractModule { |
||||
|
/* |
||||
|
Name of the module used in help documentation and logging. |
||||
|
*/ |
||||
|
name: string = "AbstractModule"; |
||||
|
|
||||
|
/* |
||||
|
Short description of the module functionality. |
||||
|
*/ |
||||
|
description: string = "Base Module That All Other Modules Extend"; |
||||
|
|
||||
|
/* |
||||
|
A helpful multiline string that defines the module usage |
||||
|
*/ |
||||
|
helpAndUsage: string = `Example: !abstract_module <command>
|
||||
|
!abstract_module <boo> : scares people`
|
||||
|
|
||||
|
/* |
||||
|
The exported command used to invoke the module directly. |
||||
|
*/ |
||||
|
command: string = "abstract_module"; |
||||
|
|
||||
|
/* |
||||
|
The default method to call when a command word is not recognized. |
||||
|
*/ |
||||
|
defaultCommand?: string = null; |
||||
|
|
||||
|
/* |
||||
|
The module should be hidden from help and command dialogs. |
||||
|
*/ |
||||
|
hidden: boolean = false; |
||||
|
|
||||
|
/* |
||||
|
This module should receive all messages, regardless of whether |
||||
|
the module was directly referenced with a command. |
||||
|
*/ |
||||
|
canHandleIndirectMessages: boolean = false; |
||||
|
|
||||
|
/* |
||||
|
Indicates if the modules needs access to the global config |
||||
|
*/ |
||||
|
needGlobalConfig: boolean = false; |
||||
|
|
||||
|
/* |
||||
|
Indicates if the module requires a readable config file. |
||||
|
*/ |
||||
|
needConfig: boolean = false; |
||||
|
|
||||
|
/* internal */ |
||||
|
|
||||
|
/* |
||||
|
The global config passed in |
||||
|
*/ |
||||
|
_global_config: any = null; |
||||
|
|
||||
|
/* |
||||
|
The loaded config file, if it exists. |
||||
|
*/ |
||||
|
_config: any = null; |
||||
|
|
||||
|
/** |
||||
|
* The recognized commands for the module |
||||
|
*/ |
||||
|
_recognizedCommands: Array<string> = []; |
||||
|
|
||||
|
/** |
||||
|
* The map of commands to functions |
||||
|
*/ |
||||
|
_recognizedCommandMap: Map<string, string> = new Map(); |
||||
|
|
||||
|
constructor(name: string, description: string, command: string) { |
||||
|
this.name = name; |
||||
|
this.description = description; |
||||
|
this.command = command; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Called after the module is initialized. |
||||
|
*/ |
||||
|
postInit() : any { |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Adds a recognized command and method to the module. |
||||
|
* |
||||
|
* @param {*} command |
||||
|
* @param {*} methodName |
||||
|
*/ |
||||
|
addRecognizedCommand(command: string, methodName: string) { |
||||
|
this._recognizedCommands.push(command); |
||||
|
this._recognizedCommandMap.set(command, methodName); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the list of recognized commands. |
||||
|
*/ |
||||
|
getRecognizedCommands() : Iterable<string> { |
||||
|
return this._recognizedCommandMap.keys(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The file path that the module configuration file is expected at. |
||||
|
*/ |
||||
|
getConfigFilePath() : string { |
||||
|
return process.env.NODE_PATH + '/data/' + this.name.toLowerCase().replace(' ', '_') + '-config.json'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fields that should be sanitized before printing the config information for the user. |
||||
|
*/ |
||||
|
getConfigSensitiveFields() : Array<string> { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return a global config value or a default. |
||||
|
* |
||||
|
* @param {*} key |
||||
|
* @param {*} defaultValue |
||||
|
*/ |
||||
|
getGlobal(key: string, defaultValue: string = null) { |
||||
|
if (this._global_config !== null && typeof this._global_config[key] !== 'undefined') { |
||||
|
return this._global_config[key]; |
||||
|
} |
||||
|
return defaultValue |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return a module config value or a default. |
||||
|
* |
||||
|
* @param {*} key |
||||
|
* @param {*} defaultValue |
||||
|
*/ |
||||
|
get(key: string, defaultValue: string = null) { |
||||
|
if (this._config !== null && typeof this._config[key] !== 'undefined') { |
||||
|
return this._config[key]; |
||||
|
} |
||||
|
return defaultValue |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Default functionality for receiving and processing a message. |
||||
|
* |
||||
|
* Override this if the module needs to do more complicated message processing. |
||||
|
*/ |
||||
|
handleMessage(event: any, room: any, callback: CallableFunction) { |
||||
|
logger.debug("[%s] [%s] [%s]", this.name, room.name, event.event.content.body); |
||||
|
|
||||
|
let messageBody = event.event.content.body; |
||||
|
let bodyParts = messageBody.split(' '); |
||||
|
let trigger = bodyParts[0]; |
||||
|
let command = bodyParts[1]; |
||||
|
var args = []; |
||||
|
if (bodyParts.length > 2) { |
||||
|
args = bodyParts.slice(2); |
||||
|
} |
||||
|
|
||||
|
logger.debug("[%s] Attempting to call %s with %s", trigger, command, args); |
||||
|
let responseMessage = this.processMessage(event, command, ...args); |
||||
|
|
||||
|
callback( |
||||
|
room, |
||||
|
responseMessage |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
Call the command method with the args |
||||
|
*/ |
||||
|
processMessage(event: any, command: string, ...args: Array<string>) { |
||||
|
if (this._recognizedCommands.includes(command)) { |
||||
|
logger.debug("Calling %s with %s", this._recognizedCommandMap.get(command), args); |
||||
|
return this[this._recognizedCommandMap.get(command)](event, ...args); |
||||
|
} else { |
||||
|
if (this.defaultCommand != null) { |
||||
|
logger.debug("Attempting to use default command %s", this.defaultCommand); |
||||
|
let newArgs = [command].concat(...args); |
||||
|
try { |
||||
|
logger.debug("Calling %s with %s", this._recognizedCommandMap.get(this.defaultCommand), newArgs); |
||||
|
return this[this._recognizedCommandMap.get(this.defaultCommand)](event, ...newArgs); |
||||
|
} catch (e) { |
||||
|
logger.error("Error while calling default command %s %s", this.defaultCommand, e); |
||||
|
return this.cmd_help(event, ...newArgs); |
||||
|
} |
||||
|
} else { |
||||
|
logger.debug("Unrecognized command %s", command); |
||||
|
return this.cmd_help(event, ...[command].concat(...args)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* Basic cmd methods */ |
||||
|
|
||||
|
/* |
||||
|
return basic help information,. |
||||
|
*/ |
||||
|
cmd_help(event: any, ...args: Array<string>) { |
||||
|
return message.createBasic(this.helpAndUsage); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the basic config file |
||||
|
* |
||||
|
* @param {...any} args |
||||
|
*/ |
||||
|
cmd_config(event: any, ...args: Array<string>) { |
||||
|
if (this._config != null) { |
||||
|
let configBody = JSON.stringify(sanitizeConfig(this._config, this.getConfigSensitiveFields()), null, 2) |
||||
|
return message.createMarkdown(configBody, "```" + configBody + "```"); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let abstractModulePrototype = Object.getPrototypeOf(new AbstractModule('', '', '')); |
||||
|
|
||||
|
/* |
||||
|
Initialization of a module. |
||||
|
*/ |
||||
|
function init(mod: AbstractModule, globalConfig: any) { |
||||
|
logger.debug("Initializing module %s", mod.name); |
||||
|
if (mod.needConfig) { |
||||
|
logger.debug("Loading config file %s", mod.getConfigFilePath()); |
||||
|
try { |
||||
|
mod._config = getConfig(mod.getConfigFilePath(), mod.getConfigSensitiveFields()); |
||||
|
} catch (e) { |
||||
|
logger.error("Module %s needs a valid config file at %s", mod.name, mod.getConfigFilePath()); |
||||
|
process.exit(1); |
||||
|
} |
||||
|
} |
||||
|
if (mod.needGlobalConfig) { |
||||
|
logger.debug("Bound global config to module %s", mod.name); |
||||
|
mod._global_config = globalConfig; |
||||
|
} |
||||
|
logger.debug("Detecting command methods."); |
||||
|
let commandMethods = getObjectKeysToPrototype(mod, abstractModulePrototype, (key) => { |
||||
|
return key.startsWith('cmd_') && isFunction(mod[key]); |
||||
|
}) |
||||
|
// let commandMethods = objectKeys.filter();
|
||||
|
logger.debug("Identified command methods: %s", commandMethods); |
||||
|
commandMethods.forEach((commandMethodName) => { |
||||
|
let command = commandMethodName.substring(4); |
||||
|
mod.addRecognizedCommand(command, commandMethodName); |
||||
|
}) |
||||
|
logger.debug("Bound command methods for %s as %s", mod.name, mod.getRecognizedCommands()); |
||||
|
mod.postInit(); |
||||
|
} |
||||
|
|
||||
|
export { AbstractModule }; |
||||
|
export { init as initModule }; |
@ -0,0 +1,65 @@ |
|||||
|
/** |
||||
|
* Administration module |
||||
|
*/ |
||||
|
|
||||
|
let { AbstractModule } = require('./abstract'); |
||||
|
let { logger } = require('../logging'); |
||||
|
let { sanitizeConfig } = require('../config'); |
||||
|
let message = require('../message'); |
||||
|
import { getBuildInfo } from '../utility'; |
||||
|
|
||||
|
class AdminModule extends AbstractModule { |
||||
|
constructor() { |
||||
|
super( |
||||
|
"Administration", |
||||
|
"Support administration tasks", |
||||
|
"admin" |
||||
|
); |
||||
|
this.hidden = true; |
||||
|
this.helpAndUsage = `Usage: admin <command>
|
||||
|
admin config - print the bot configuration`;
|
||||
|
this.needGlobalConfig = true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Override to only permit recognized admin users to access the plugin |
||||
|
*/ |
||||
|
handleMessage(event: any, room: any, callback: CallableFunction) { |
||||
|
if (this.getGlobal("admins", []).includes(event.sender.userId)) { |
||||
|
logger.debug("Authorized %s for admin action", event.sender.userId); |
||||
|
super.handleMessage(event, room, callback); |
||||
|
} else { |
||||
|
logger.warn("User %s tried to access admin functionality", event.sender.userId); |
||||
|
return event.sender.userId + " is not a recognized admin!" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* Commands |
||||
|
* All methods starting with cmd_ will be parsed at initialization to expose those methods as commdands to the user |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Print the current config information |
||||
|
* |
||||
|
* @param {...any} args |
||||
|
*/ |
||||
|
cmd_config(event: any, ...args: Array<string>) { |
||||
|
if (this._global_config != null) { |
||||
|
let configBody = JSON.stringify(sanitizeConfig(this._global_config), null, 2) |
||||
|
return message.createMarkdown(configBody, "```" + configBody + "```"); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return the verion of the running bot |
||||
|
* |
||||
|
* @param event |
||||
|
* @param args |
||||
|
*/ |
||||
|
cmd_version(event: any, ...args: Array<string>) { |
||||
|
return getBuildInfo(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { AdminModule as Module }; |
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* Example module |
||||
|
* Copy this, replace the relevent parts, add it to the list in index.js |
||||
|
*/ |
||||
|
|
||||
|
let { AbstractModule } = require('./abstract'); |
||||
|
|
||||
|
class ExampleModule extends AbstractModule { |
||||
|
constructor() { |
||||
|
super( |
||||
|
"Example", |
||||
|
"Example tasks", |
||||
|
"example" |
||||
|
); |
||||
|
this.helpAndUsage = `Usage: example <command>
|
||||
|
...`;
|
||||
|
this.hidden = false; //default value
|
||||
|
this.needGlobalConfig = false; // default value
|
||||
|
this.needConfig = false; // default value
|
||||
|
this.defaultCommand = 'search'; // name of the command to call when no recognized command name is present
|
||||
|
} |
||||
|
|
||||
|
/* Commands |
||||
|
* All methods starting with cmd_ will be parsed at initialization to expose those methods as commdands to the user |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Processed when !example blah is processed |
||||
|
* |
||||
|
* @param {...any} args the individual words passed after the command |
||||
|
*/ |
||||
|
cmd_blah(event, ...args) { |
||||
|
return "Bla blah bla" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let module = new ExampleModule(); |
||||
|
|
||||
|
export { module }; |
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* Giphy module |
||||
|
*/ |
||||
|
|
||||
|
let { AbstractModule } = require('./abstract'); |
||||
|
let axios = require('axios'); |
||||
|
let { logger } = require('../logging'); |
||||
|
|
||||
|
class GiphyModule extends AbstractModule { |
||||
|
constructor() { |
||||
|
super( |
||||
|
"Giphy", |
||||
|
"Insert Giphy Links/Media", |
||||
|
"giphy" |
||||
|
); |
||||
|
this.helpAndUsage = `Usage: !giphy itsworking
|
||||
|
...`;
|
||||
|
this.needConfig = true; |
||||
|
this.defaultCommand = 'search'; |
||||
|
} |
||||
|
|
||||
|
getConfigSensitiveFields(): Array<string> { |
||||
|
return ["apiKey"]; |
||||
|
} |
||||
|
|
||||
|
getGiphySearch(term: string) { |
||||
|
let url = this.get("endpoint") + '/gifs/search?api_key=' + this.get("apiKey") + '&q=' + term + '&limit=1'; |
||||
|
logger.debug("Requesting: %s", url.replace(this.get("apiKey"), '******')); |
||||
|
return axios.get(url); |
||||
|
} |
||||
|
|
||||
|
/* Commands |
||||
|
* All methods starting with cmd_ will be parsed at initialization to expose those methods as commdands to the user |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Return the top item for the search terms. |
||||
|
* |
||||
|
* @param {...any} args |
||||
|
*/ |
||||
|
cmd_search(event: any, ...args: Array<string>) { |
||||
|
return this.getGiphySearch(args[0]) |
||||
|
.then((response) => { |
||||
|
// logger.debug("Giphy response: %o", response.data.data[0].url);
|
||||
|
return response.data.data[0].embed_url; |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { GiphyModule as Module }; |
@ -0,0 +1,52 @@ |
|||||
|
/** |
||||
|
* Help module |
||||
|
*/ |
||||
|
|
||||
|
let { AbstractModule } = require('./abstract'); |
||||
|
let { logger } = require('../logging'); |
||||
|
|
||||
|
class HelpModule extends AbstractModule { |
||||
|
constructor(commandMap: Map<string, CallableFunction>) { |
||||
|
super( |
||||
|
"Help", |
||||
|
"Provide helpful information about other modules.", |
||||
|
"help" |
||||
|
); |
||||
|
this._commandMap = commandMap; |
||||
|
this._commandList = Array.from(commandMap.keys()).sort(); |
||||
|
this.defaultCommand = 'help'; |
||||
|
} |
||||
|
|
||||
|
_default_help_message(): string { |
||||
|
let help = `!help <command>`; |
||||
|
for (let command of this._commandList) { |
||||
|
help += "\n!help " + command + " : " + this._commandMap.get(command).description; |
||||
|
} |
||||
|
return help; |
||||
|
} |
||||
|
|
||||
|
/* Commands */ |
||||
|
|
||||
|
cmd_help(event: any, ...args: Array<string>) { |
||||
|
logger.debug("%o", args) |
||||
|
if (args.length < 1) { |
||||
|
return this._default_help_message(); |
||||
|
} else { |
||||
|
let command = args[0]; |
||||
|
logger.debug("Looking up help for %s from %o", command, this._commandMap); |
||||
|
if (this._commandList.includes(command)) { |
||||
|
return this._commandMap.get(command).cmd_help(); |
||||
|
} else { |
||||
|
let help = command + " is an unrecognized module\n"; |
||||
|
help += this._default_help_message(); |
||||
|
return help; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function create(commandMap: Map<string, CallableFunction>) { |
||||
|
return new HelpModule(commandMap); |
||||
|
} |
||||
|
|
||||
|
export { create }; |
@ -0,0 +1,19 @@ |
|||||
|
/** |
||||
|
* Manage the registered modules |
||||
|
*/ |
||||
|
|
||||
|
let admin = require('./admin'); |
||||
|
let giphy = require('./giphy') |
||||
|
let szurubooru = require('./szurubooru/index'); |
||||
|
|
||||
|
function getModules() { |
||||
|
return [ |
||||
|
new admin.Module(), |
||||
|
new giphy.Module(), |
||||
|
new szurubooru.Module() |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
let modules = getModules(); |
||||
|
|
||||
|
export { modules }; |
@ -0,0 +1,135 @@ |
|||||
|
/** |
||||
|
* Szurubooru api client |
||||
|
*/ |
||||
|
|
||||
|
let axios = require('axios'); |
||||
|
let { logger } = require('../../logging'); |
||||
|
|
||||
|
function getRandomInt(min, max) { |
||||
|
min = Math.ceil(min); |
||||
|
max = Math.floor(max); |
||||
|
return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
|
||||
|
} |
||||
|
|
||||
|
export class SzurubooruClient { |
||||
|
|
||||
|
url: string; |
||||
|
baseUrl: string; |
||||
|
username: string; |
||||
|
token: string; |
||||
|
|
||||
|
randomImageCount: number; |
||||
|
|
||||
|
_randomSearchQueryValues: string = "type:image,animated,video sort:random"; |
||||
|
_randomSearchUrlTemplate: string = "%s/posts/?offset=%s&limit=%d&query=%s"; |
||||
|
|
||||
|
constructor(url: string, username: string, token: string) { |
||||
|
this.url = url; |
||||
|
this.baseUrl = url.replace('/api', ''); |
||||
|
this.username = username; |
||||
|
this.token = token; |
||||
|
this.updateRandomScope(); |
||||
|
} |
||||
|
|
||||
|
getRandomSearchUrl(offset: number, limit: number, tags: Array<string> = []) { |
||||
|
var searchQuery = this._randomSearchQueryValues; |
||||
|
if (tags !== null && tags.length > 0) { |
||||
|
searchQuery = this._randomSearchQueryValues + tags.join(" "); |
||||
|
} |
||||
|
return this.url + '/posts/?offset=' + offset + '&limit=' + limit + '&query=' + searchQuery; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determine a sane range of offset values |
||||
|
* |
||||
|
* @param {*} limit The content limit of a single page |
||||
|
* @param {*} total The number of objects to search through |
||||
|
*/ |
||||
|
getValidOffsetRange(limit: number, total: number): Array<number> { |
||||
|
let offsetOverflow = total % limit; |
||||
|
var offsetCeiling = (Math.round(total / limit)); |
||||
|
if (offsetOverflow === 0) { |
||||
|
offsetCeiling -= 1; |
||||
|
} |
||||
|
return [0, offsetCeiling]; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Determine a random offset based on the potential random search size |
||||
|
* |
||||
|
* Because Szurubooru doesn't randomize each sort:random search request, we must |
||||
|
* manually calculate a random offset for each request to simulate randomness in the |
||||
|
* posts returned. |
||||
|
* |
||||
|
* @param {*} self |
||||
|
* @param {*} imageCount |
||||
|
*/ |
||||
|
getRandomSearchParameters(imageCount?: number) { |
||||
|
let resultLimit = 10; |
||||
|
var offsetRange = [0, 1]; |
||||
|
if (imageCount === null) { |
||||
|
offsetRange = this.getValidOffsetRange(resultLimit, this.randomImageCount); |
||||
|
} else { |
||||
|
offsetRange = this.getValidOffsetRange(resultLimit, imageCount); |
||||
|
} |
||||
|
return [resultLimit, getRandomInt(offsetRange[0], offsetRange[1])]; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
async updateRandomScope() { |
||||
|
let client = this; |
||||
|
let target = this.getRandomSearchUrl(0, 1); |
||||
|
await this.makeGetRequest(target).then((result: any) => { |
||||
|
client.randomImageCount = result.total; |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
makeGetRequest(url: string) { |
||||
|
logger.debug("Making get request to %s", url); |
||||
|
return axios.get(url, { |
||||
|
headers: { |
||||
|
"Authorization": "Token " + Buffer.from(this.username + ':' + this.token).toString('base64'), |
||||
|
"Accept": "application/json" |
||||
|
} |
||||
|
}).then((response: any) => { |
||||
|
logger.debug("Recieved: %o", response.data); |
||||
|
return response.data; |
||||
|
}, (err: any) => { |
||||
|
logger.error("Unexpected client error: %o", err); |
||||
|
return null; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return a random post matching the optionally provided tags. |
||||
|
* |
||||
|
* @param {*} self |
||||
|
* @param {*} tags |
||||
|
*/ |
||||
|
getRandomPost(tags: Array<string> = []) { |
||||
|
let client = this; |
||||
|
var resultLimit = 0; |
||||
|
var randomSearchOffset = 0; |
||||
|
if (tags.length === 0) { |
||||
|
let randomSearchParameters = this.getRandomSearchParameters(); |
||||
|
resultLimit = randomSearchParameters[0]; |
||||
|
randomSearchOffset = randomSearchParameters[1]; |
||||
|
} |
||||
|
let searchUrl = this.getRandomSearchUrl(randomSearchOffset, resultLimit, tags); |
||||
|
return this.makeGetRequest(searchUrl) |
||||
|
.then((data: any) => { |
||||
|
if (data !== null) { |
||||
|
let limitSpace = Math.min(resultLimit, data.results.length - 1); |
||||
|
let randomPost = data.results[getRandomInt(0, limitSpace)]; |
||||
|
return client.baseUrl + '/' + randomPost.contentUrl; |
||||
|
} else { |
||||
|
return null |
||||
|
} |
||||
|
}, (err: any) => { |
||||
|
logger.error("Unexpected error: %o", err); |
||||
|
return null; |
||||
|
}) |
||||
|
} |
||||
|
} |
@ -0,0 +1,65 @@ |
|||||
|
/** |
||||
|
* Module for interacting with a Szurubooru instance |
||||
|
*/ |
||||
|
|
||||
|
let { AbstractModule } = require('../abstract'); |
||||
|
let { logger } = require('../../logging'); |
||||
|
import { SzurubooruClient } from './client'; |
||||
|
|
||||
|
class SzurubooruModule extends AbstractModule { |
||||
|
|
||||
|
client: SzurubooruClient; |
||||
|
|
||||
|
constructor() { |
||||
|
super( |
||||
|
"Szurubooru", |
||||
|
"Interact with a Szurubooru instance", |
||||
|
"szurubooru" |
||||
|
); |
||||
|
this.helpAndUsage = `Usage:
|
||||
|
'!szurubooru <tag> [<tag>...]' display a random post that matches tags |
||||
|
'!szurubooru config' prints config information for the room |
||||
|
'!szurubooru config upload' toggles uploading images from room messages |
||||
|
'!szurubooru iqdb' get an IQDB link for the last displayed post |
||||
|
'!szurubooru need [<max tag count (5)>]' show a random post that needs additional tags |
||||
|
'!szurubooru post <post_id>' retrieve a post and publish the image to the room |
||||
|
'!szurubooru random [<tag>...]' display a random image from szurubooru that matches tags if preset |
||||
|
'!szurubooru random update' update the random counts |
||||
|
'!szurubooru recent [<days> (1)]' display a recent random image within the last <days> |
||||
|
'!szurubooru related' display related posts to the last displayed post |
||||
|
'!szurubooru tag <tag> [<tag>...]' add tags to the last displayed post |
||||
|
'!szurubooru tag clone <post_id>' clone tags from <post_id> to the last displayed post |
||||
|
'!szurubooru tag remove <tag> [<tag>...]' remove tags from the last displayed post |
||||
|
'!szurubooru tags' list of all tags by popularity |
||||
|
'!szurubooru tags <tag_stub> [<tag_stub>...]' list tags similar to each <tag_stub> |
||||
|
'!szurubooru upload <url>' upload a remote file to szurubooru`;
|
||||
|
this.needConfig = true; |
||||
|
this.defaultCommand = 'random'; |
||||
|
} |
||||
|
|
||||
|
postInit() { |
||||
|
super.postInit(); |
||||
|
this.client = new SzurubooruClient( |
||||
|
this.get("url"), |
||||
|
this.get("username"), |
||||
|
this.get("token") |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
getConfigSensitiveFields() { |
||||
|
return ["token"]; |
||||
|
} |
||||
|
|
||||
|
/* Commands |
||||
|
* All methods starting with cmd_ will be parsed at initialization to expose those methods as commdands to the user |
||||
|
*/ |
||||
|
|
||||
|
cmd_random(event: any, ...args: Array<string>) { |
||||
|
return this.client.getRandomPost().then((data) => { |
||||
|
logger.info("Random post: %o", data); |
||||
|
return data; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { SzurubooruModule as Module }; |
@ -0,0 +1,85 @@ |
|||||
|
let fs = require('fs'); |
||||
|
let { logger } = require('./logging'); |
||||
|
|
||||
|
function getShortestPrefix(radixTree: any, key: string, sliceSize: number) { |
||||
|
let shortKey = key.substring(0, sliceSize); |
||||
|
let keyCount = radixTree.countPrefix(shortKey); |
||||
|
if (keyCount < 1) { |
||||
|
return null; |
||||
|
} |
||||
|
if (key.length === sliceSize && radixTree.getPrefix(shortKey).includes(key)) { |
||||
|
return null; |
||||
|
} |
||||
|
if (keyCount === 1) { |
||||
|
return shortKey; |
||||
|
} |
||||
|
return getShortestPrefix(radixTree, key, sliceSize + 1); |
||||
|
} |
||||
|
|
||||
|
function toISODateString(d: Date) { |
||||
|
function pad(n: number) { return n < 10 ? '0' + n : n } |
||||
|
return d.getUTCFullYear() + '-' |
||||
|
+ pad(d.getUTCMonth() + 1) + '-' |
||||
|
+ pad(d.getUTCDate()) + 'T' |
||||
|
+ pad(d.getUTCHours()) + ':' |
||||
|
+ pad(d.getUTCMinutes()) + ':' |
||||
|
+ pad(d.getUTCSeconds()) + 'Z' |
||||
|
} |
||||
|
|
||||
|
function getBuildInfo() { |
||||
|
let buildInfoPath = process.env.NODE_PATH + '/build.info'; |
||||
|
try { |
||||
|
return fs.readFileSync(buildInfoPath, "utf8").trim(); |
||||
|
} catch (err) { |
||||
|
if (err.code === 'ENOENT') { |
||||
|
return "UNKNOWN_" + toISODateString(new Date()); |
||||
|
} else { |
||||
|
logger.error("Unexpected Error!", err); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function sleep(ms: number) { |
||||
|
return new Promise(resolve => { |
||||
|
setTimeout(resolve, ms) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function isString(s: any) { |
||||
|
return typeof (s) === 'string' || s instanceof String; |
||||
|
} |
||||
|
|
||||
|
function isFunction(f: any) { |
||||
|
return f && {}.toString.call(f) === '[object Function]'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parse the prototype tree to return all accessible properties till |
||||
|
* reaching a sentinelPrototype. |
||||
|
* |
||||
|
* Optionally provide a filtering function to return only the names that match. |
||||
|
* |
||||
|
* @param {*} initialObj The starting object to derive the from |
||||
|
* @param {*} sentinelPrototype The prototype that represents the end of the line |
||||
|
* @param {*} filterFunc A filtering function for the return names |
||||
|
*/ |
||||
|
function getObjectKeysToPrototype(initialObj: any, sentinelPrototype: any, filterFunc: any = (e: any) => true) { |
||||
|
let prototypeChain = [] |
||||
|
var targetPrototype = initialObj; |
||||
|
while (Object.getPrototypeOf(targetPrototype) && targetPrototype !== sentinelPrototype) { |
||||
|
targetPrototype = Object.getPrototypeOf(targetPrototype); |
||||
|
prototypeChain.push(targetPrototype); |
||||
|
} |
||||
|
let completePropertyNames = prototypeChain.map((obj) => { |
||||
|
return Object.getOwnPropertyNames(obj); |
||||
|
}) |
||||
|
return [Object.getOwnPropertyNames(initialObj)].concat.apply([], completePropertyNames).filter(filterFunc); |
||||
|
} |
||||
|
|
||||
|
export { getShortestPrefix }; |
||||
|
export { toISODateString }; |
||||
|
export { getBuildInfo }; |
||||
|
export { sleep }; |
||||
|
export { isString }; |
||||
|
export { isFunction }; |
||||
|
export { getObjectKeysToPrototype }; |
@ -0,0 +1,14 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"outDir": "./dist", |
||||
|
"allowJs": false, |
||||
|
"target": "ES2015", |
||||
|
"module": "CommonJS", |
||||
|
"typeRoots": [ |
||||
|
"node_modules/@types" |
||||
|
] |
||||
|
}, |
||||
|
"include": [ |
||||
|
"./src/**/*" |
||||
|
] |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue