Add electron
This commit is contained in:
13
backend/src/DataSource/DataSource.ts
Normal file
13
backend/src/DataSource/DataSource.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DataSourceState } from './'
|
||||
|
||||
type MessageCallback = (topic: string, payload: Buffer) => void
|
||||
|
||||
// A DataSource should automatically reconnect if connection was broken
|
||||
interface DataSource<DataSourceOptions> {
|
||||
connect(options: DataSourceOptions): DataSourceState
|
||||
disconnect(): void
|
||||
onMessage(messageCallback: MessageCallback): void
|
||||
topicSeparator: string
|
||||
}
|
||||
|
||||
export { DataSource, MessageCallback }
|
||||
41
backend/src/DataSource/DataSourceState.ts
Normal file
41
backend/src/DataSource/DataSourceState.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
interface InternalState {
|
||||
connecting: boolean
|
||||
connected: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export class DataSourceState {
|
||||
private state: InternalState = {
|
||||
error: undefined,
|
||||
connected: false,
|
||||
connecting: false
|
||||
}
|
||||
|
||||
public setConnected(connected: boolean) {
|
||||
this.state = {
|
||||
error: undefined,
|
||||
connected: connected,
|
||||
connecting: false
|
||||
}
|
||||
}
|
||||
|
||||
public setError(error: Error) {
|
||||
this.state = {
|
||||
error: error,
|
||||
connected: false,
|
||||
connecting: false
|
||||
}
|
||||
}
|
||||
|
||||
public setConnecting() {
|
||||
this.state = {
|
||||
error: undefined,
|
||||
connected: false,
|
||||
connecting: true
|
||||
}
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
return this.state
|
||||
}
|
||||
}
|
||||
58
backend/src/DataSource/MqttSource.ts
Normal file
58
backend/src/DataSource/MqttSource.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Client, connect as mqttConnect } from 'mqtt'
|
||||
import { DataSource, DataSourceState } from './'
|
||||
|
||||
export interface MqttOptions {
|
||||
url: string
|
||||
}
|
||||
|
||||
export class MqttSource implements DataSource<MqttOptions> {
|
||||
private client: Client | undefined
|
||||
private messageCallback?: (topic: string, message: Buffer) => void
|
||||
private rootSubscription = '#'
|
||||
public topicSeparator = '/'
|
||||
|
||||
public onMessage(messageCallback: (topic: string, message: Buffer) => void) {
|
||||
this.messageCallback = messageCallback
|
||||
}
|
||||
|
||||
public connect(options: MqttOptions): DataSourceState {
|
||||
const state = new DataSourceState()
|
||||
|
||||
const client = mqttConnect(options.url, {
|
||||
resubscribe: false
|
||||
})
|
||||
|
||||
this.client = client
|
||||
|
||||
client.on('error', (error: Error) => {
|
||||
state.setError(error)
|
||||
})
|
||||
|
||||
client.on('close', () => {
|
||||
state.setConnected(false)
|
||||
})
|
||||
|
||||
client.on('reconnect', () => {
|
||||
state.setConnecting()
|
||||
})
|
||||
|
||||
client.on('connect', () => {
|
||||
state.setConnected(true)
|
||||
client.subscribe(this.rootSubscription, (err: Error) => {
|
||||
if (err) {
|
||||
throw new Error('mqtt subscription failed')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
client.on('message', (topic, message) => {
|
||||
this.messageCallback && this.messageCallback(topic, message)
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.client && this.client.end()
|
||||
}
|
||||
}
|
||||
10
backend/src/DataSource/index.ts
Normal file
10
backend/src/DataSource/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DataSource } from './DataSource'
|
||||
import { DataSourceState } from './DataSourceState'
|
||||
import { MqttOptions, MqttSource } from './MqttSource'
|
||||
|
||||
export {
|
||||
DataSource,
|
||||
DataSourceState,
|
||||
MqttOptions,
|
||||
MqttSource,
|
||||
}
|
||||
61
backend/src/DotExport.ts
Normal file
61
backend/src/DotExport.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Tree, TreeNode } from './Model'
|
||||
|
||||
export class DotExport {
|
||||
public static renderNodeInformation(node: TreeNode): string {
|
||||
return `\t${node.sourceEdge.hash()} [label=${this.renderLabel(node.value)}]`
|
||||
}
|
||||
public static toDot(tree: Tree): string {
|
||||
let i = 1
|
||||
let leaveEdges = Object.values(tree.edges)
|
||||
.map(e => e.node)
|
||||
.map(node => node.leafes())
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
.map(leave => leave.branch())
|
||||
|
||||
const allEdges: Array<string> = []
|
||||
const nodeInformation: {[s: string]: string} = {}
|
||||
leaveEdges.map(edges => edges.reduce( (prev, current) => {
|
||||
let currentHash = current.sourceEdge.hash()
|
||||
nodeInformation[currentHash] = this.renderNodeInformation(current)
|
||||
if (current && prev) {
|
||||
allEdges.push(`\t${prev.sourceEdge.hash()} -> ${currentHash} [label=${this.renderLabel(current.sourceEdge.name)}]`)
|
||||
}
|
||||
return current
|
||||
}))
|
||||
|
||||
return `strict digraph ethane {
|
||||
${
|
||||
[this.renderNodeInformation(tree)]
|
||||
.concat(Object.values(nodeInformation))
|
||||
.concat(allEdges)
|
||||
.join('\n')
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
private static renderLabel(value: any): string {
|
||||
let str;
|
||||
if(!isNaN(value)) {
|
||||
str = value
|
||||
} else {
|
||||
str = JSON.stringify(value)
|
||||
if(str && str.length > 0) {
|
||||
str = str.slice(1, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!str) {
|
||||
return '""'
|
||||
}
|
||||
|
||||
if(str.length > 20) {
|
||||
str = str.slice(0, 20)+'…'
|
||||
}
|
||||
|
||||
if (str[0] !== '"') {
|
||||
str = `"${str}"`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
35
backend/src/Model/Edge.ts
Normal file
35
backend/src/Model/Edge.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Hashable, TreeNode } from './'
|
||||
const sha1 = require('sha1')
|
||||
|
||||
export class Edge implements Hashable {
|
||||
public name: string
|
||||
|
||||
public node!: TreeNode
|
||||
public source?: TreeNode | undefined
|
||||
private cachedHash?: string
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
public edges() {
|
||||
return this.node ? Object.values(this.node.edges) : []
|
||||
}
|
||||
|
||||
public hash(): string {
|
||||
if (!this.cachedHash) {
|
||||
let previousHash = (this.source && this.source.sourceEdge) ? this.source.sourceEdge.hash() : ''
|
||||
this.cachedHash = 'H' + sha1(previousHash + this.name)
|
||||
}
|
||||
|
||||
return this.cachedHash
|
||||
}
|
||||
|
||||
public firstEdge(): Edge {
|
||||
if (this.source && this.source.sourceEdge) {
|
||||
return this.source.sourceEdge.firstEdge()
|
||||
} else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
3
backend/src/Model/Hashable.ts
Normal file
3
backend/src/Model/Hashable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Hashable {
|
||||
hash(): string
|
||||
}
|
||||
4
backend/src/Model/TopicProperties.ts
Normal file
4
backend/src/Model/TopicProperties.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class TopicProperties {
|
||||
public topicSeparator: string = '/'
|
||||
public multilevelWildcard: string | null = '#'
|
||||
}
|
||||
7
backend/src/Model/Tree.ts
Normal file
7
backend/src/Model/Tree.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Edge, TreeNode } from './'
|
||||
|
||||
export class Tree extends TreeNode {
|
||||
constructor() {
|
||||
super(undefined, undefined)
|
||||
}
|
||||
}
|
||||
83
backend/src/Model/TreeNode.ts
Normal file
83
backend/src/Model/TreeNode.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Edge } from './'
|
||||
import { EventEmitter } from 'events'
|
||||
|
||||
export class TreeNode extends EventEmitter {
|
||||
public sourceEdge?: Edge
|
||||
public value?: any | null
|
||||
public edges: {[s: string]: Edge} = {}
|
||||
public collapsed = false
|
||||
|
||||
constructor(sourceEdge?: Edge, value?: any) {
|
||||
super()
|
||||
|
||||
if (sourceEdge) {
|
||||
this.sourceEdge = sourceEdge
|
||||
sourceEdge.node = this
|
||||
}
|
||||
this.value = value
|
||||
}
|
||||
|
||||
public hash(): string {
|
||||
return 'N' + (this.sourceEdge ? this.sourceEdge.hash() : '')
|
||||
}
|
||||
|
||||
public firstNode(): TreeNode {
|
||||
return this.sourceEdge ? this.sourceEdge.firstEdge().node : this
|
||||
}
|
||||
|
||||
public path(): string {
|
||||
return this.branch()
|
||||
.map(node => (node.sourceEdge && node.sourceEdge.name))
|
||||
.filter(name => name !== undefined)
|
||||
.join('/')
|
||||
}
|
||||
|
||||
private previous(): TreeNode | undefined {
|
||||
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
|
||||
}
|
||||
|
||||
public addEdge(edge: Edge) {
|
||||
this.edges[edge.name] = edge
|
||||
edge.source = this
|
||||
this.emit('update')
|
||||
}
|
||||
|
||||
public branch(): Array<TreeNode> {
|
||||
let previous = this.previous()
|
||||
if (!previous) {
|
||||
return [this]
|
||||
}
|
||||
|
||||
return previous.branch().concat([this])
|
||||
}
|
||||
|
||||
public updateWithNode(node: TreeNode) {
|
||||
if (node.value !== undefined) {
|
||||
this.value = node.value
|
||||
}
|
||||
this.mergeEdges(node)
|
||||
this.emit('update')
|
||||
}
|
||||
|
||||
public leafes(): Array<TreeNode> {
|
||||
if (Object.values(this.edges).length === 0) {
|
||||
return [this]
|
||||
}
|
||||
|
||||
return Object.values(this.edges)
|
||||
.map(e => e.node.leafes())
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
}
|
||||
|
||||
private mergeEdges(node: TreeNode) {
|
||||
let edgeKeys = Object.keys(node.edges)
|
||||
for (let edgeKey of edgeKeys) {
|
||||
let matchingEdge = this.edges[edgeKey]
|
||||
if (matchingEdge) {
|
||||
matchingEdge.node.updateWithNode(node.edges[edgeKey].node)
|
||||
} else {
|
||||
this.addEdge(node.edges[edgeKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
backend/src/Model/TreeNodeFactory.ts
Normal file
32
backend/src/Model/TreeNodeFactory.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Edge, Tree, TreeNode } from './'
|
||||
|
||||
export abstract class TreeNodeFactory {
|
||||
public static fromEdgesAndValue(edgeNames: Array<string>, value: any): TreeNode {
|
||||
const lastEdgeIndex = edgeNames.length - 1
|
||||
var edges = edgeNames
|
||||
.map((name, idx) => {
|
||||
const edge = new Edge(name)
|
||||
|
||||
const nodeValue = lastEdgeIndex == idx ? value : undefined
|
||||
const node = new TreeNode(edge, nodeValue)
|
||||
edge.node = node
|
||||
return edge
|
||||
})
|
||||
|
||||
let reversed: Array<Edge> = edges.reverse()
|
||||
let previous: Edge | undefined = undefined;
|
||||
for (let edge of reversed) {
|
||||
if (previous) {
|
||||
edge.node.addEdge(previous)
|
||||
}
|
||||
previous = edge;
|
||||
}
|
||||
|
||||
let leaf = reversed[0].node
|
||||
|
||||
let sourceTree = new Tree()
|
||||
sourceTree.updateWithNode(leaf.firstNode())
|
||||
|
||||
return leaf
|
||||
}
|
||||
}
|
||||
10
backend/src/Model/index.ts
Normal file
10
backend/src/Model/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Edge } from './Edge'
|
||||
import { TreeNode } from './TreeNode'
|
||||
import { TreeNodeFactory } from './TreeNodeFactory'
|
||||
import { Tree } from './Tree'
|
||||
import { TopicProperties } from './TopicProperties'
|
||||
import { Hashable } from './Hashable'
|
||||
|
||||
export {
|
||||
Edge, TreeNode, TreeNodeFactory, Tree, TopicProperties, Hashable
|
||||
}
|
||||
31
backend/src/Model/spec/Edge.spec.ts
Normal file
31
backend/src/Model/spec/Edge.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Edge, TreeNode } from '../'
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
describe('Edge', () => {
|
||||
|
||||
it('should contain a name', () => {
|
||||
let e = new Edge('foo')
|
||||
expect(e.name).to.equal('foo')
|
||||
});
|
||||
|
||||
it('hash should not be empty', () => {
|
||||
let e = new Edge('bar')
|
||||
expect(e.hash().length).to.be.gt(0)
|
||||
});
|
||||
|
||||
it('hash should be stable', () => {
|
||||
let e = new Edge('bar')
|
||||
let previousHash = e.hash()
|
||||
expect(e.hash()).to.eq(previousHash)
|
||||
});
|
||||
|
||||
it('hash should change when parent is present', () => {
|
||||
let foo = new Edge('foo')
|
||||
let bar = new Edge('bar')
|
||||
|
||||
var previousHash = bar.hash()
|
||||
bar.source = new TreeNode(foo, undefined)
|
||||
expect(bar.hash()).to.not.eq(previousHash)
|
||||
});
|
||||
});
|
||||
18
backend/src/Model/spec/Tree.spec.ts
Normal file
18
backend/src/Model/spec/Tree.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Edge, Tree, TreeNode, TreeNodeFactory } from '../'
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import './TreeNode.findNode'
|
||||
|
||||
describe('Tree', () => {
|
||||
it('node can be merged into a tree', () => {
|
||||
const tree = new Tree()
|
||||
|
||||
const topics = 'foo/bar'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 3)
|
||||
debugger
|
||||
tree.updateWithNode(leaf.firstNode())
|
||||
let expectedNode = tree.findNode('foo/bar')
|
||||
expect(expectedNode).to.eq(leaf)
|
||||
})
|
||||
});
|
||||
29
backend/src/Model/spec/TreeNode.findNode.spec.ts
Normal file
29
backend/src/Model/spec/TreeNode.findNode.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { TreeNode, TreeNodeFactory } from '../'
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import './TreeNode.findNode'
|
||||
|
||||
describe('TreeNode.findNode', () => {
|
||||
it('findNode should retrieve node', () => {
|
||||
const topics = 'foo/bar/baz'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 5)
|
||||
|
||||
let root = leaf.firstNode()
|
||||
expect(root.sourceEdge.name).to.eq('')
|
||||
|
||||
let barNode = root.findNode('foo/bar')
|
||||
if (!barNode) {
|
||||
expect.fail('did not find node')
|
||||
return
|
||||
}
|
||||
expect(barNode.sourceEdge.name).to.eq('bar')
|
||||
|
||||
let bazNode = root.findNode('foo/bar/baz')
|
||||
if (!bazNode) {
|
||||
expect.fail('did not find node')
|
||||
return
|
||||
}
|
||||
expect(bazNode.sourceEdge.name).to.eq('baz')
|
||||
})
|
||||
})
|
||||
20
backend/src/Model/spec/TreeNode.findNode.ts
Normal file
20
backend/src/Model/spec/TreeNode.findNode.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TreeNode } from '../'
|
||||
|
||||
declare module "../" {
|
||||
interface TreeNode {
|
||||
findNode(path: String): TreeNode | undefined
|
||||
}
|
||||
}
|
||||
|
||||
TreeNode.prototype.findNode = function(path: String): TreeNode | undefined {
|
||||
const topics = path.split('/')
|
||||
let edge = this.edges[topics[0]]
|
||||
let remainingTopics = topics.slice(1, topics.length)
|
||||
if (edge && remainingTopics.length === 0) {
|
||||
return edge.node
|
||||
} else if (edge) {
|
||||
return edge.node.findNode(remainingTopics.join('/'))
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
51
backend/src/Model/spec/TreeNode.spec.ts
Normal file
51
backend/src/Model/spec/TreeNode.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Edge, Tree, TreeNode, TreeNodeFactory } from '../'
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import './TreeNode.findNode'
|
||||
|
||||
describe('TreeNode', () => {
|
||||
it('updateWithNode should update value', () => {
|
||||
const topics = 'foo/bar'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 3)
|
||||
expect(leaf.value).to.eq(3)
|
||||
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics, 5)
|
||||
leaf.firstNode().updateWithNode(updateLeave.firstNode())
|
||||
|
||||
expect(leaf.firstNode().sourceEdge.name).to.eq(updateLeave.firstNode().sourceEdge.name)
|
||||
expect(leaf.value).to.eq(5)
|
||||
})
|
||||
|
||||
it('updateWithNode should update intermediate nodes', () => {
|
||||
const topics1 = 'foo/bar/baz'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics1, 3)
|
||||
expect(leaf.value).to.eq(3)
|
||||
|
||||
const topics2 = 'foo/bar'.split('/')
|
||||
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics2, 5)
|
||||
leaf.firstNode().updateWithNode(updateLeave.firstNode())
|
||||
|
||||
let barNode = leaf.firstNode().findNode('foo/bar')
|
||||
expect(barNode && barNode.sourceEdge.name).to.eq('bar')
|
||||
expect(barNode && barNode.value).to.eq(5)
|
||||
|
||||
expect(leaf.sourceEdge.name).to.eq('baz')
|
||||
expect(leaf.value).to.eq(3)
|
||||
})
|
||||
|
||||
it('updateWithNode should add nodes to the tree', () => {
|
||||
const topics1 = 'foo/bar'.split('/')
|
||||
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, 3)
|
||||
|
||||
const topics2 = 'foo/bar/baz'.split('/')
|
||||
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, 5)
|
||||
|
||||
leaf1.firstNode().updateWithNode(leaf2.firstNode())
|
||||
|
||||
let expectedNode = leaf1.firstNode().findNode('foo/bar/baz')
|
||||
if (!expectedNode) {
|
||||
expect.fail('merge seems to have failed')
|
||||
return
|
||||
}
|
||||
})
|
||||
});
|
||||
46
backend/src/Model/spec/TreeNodeFactory.spec.ts
Normal file
46
backend/src/Model/spec/TreeNodeFactory.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Edge, TreeNode, TreeNodeFactory } from '../'
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
describe('TreeNodeFactory', () => {
|
||||
it('should create node', () => {
|
||||
let topic = 'foo/bar'
|
||||
let edges = topic.split('/')
|
||||
let node = TreeNodeFactory.fromEdgesAndValue(edges, 5)
|
||||
|
||||
expect(node).to.not.eq(undefined)
|
||||
expect(node.sourceEdge.name).to.eq('bar')
|
||||
expect(node.value).to.eq(5)
|
||||
|
||||
if (!node.sourceEdge.source) {
|
||||
expect.fail('should not happen')
|
||||
return
|
||||
}
|
||||
|
||||
let foo = node.sourceEdge.source.sourceEdge
|
||||
expect(foo.name).to.eq('foo')
|
||||
});
|
||||
|
||||
it('node should contain edges in order', () => {
|
||||
let topic = 'foo/bar/baz'
|
||||
let edges = topic.split('/')
|
||||
let node = TreeNodeFactory.fromEdgesAndValue(edges, 5)
|
||||
|
||||
expect(node.value).to.eq(5)
|
||||
expect(node.sourceEdge.name).to.eq('baz')
|
||||
|
||||
const barNode = node.sourceEdge.source
|
||||
if (!barNode) {
|
||||
expect.fail('should not fail')
|
||||
return
|
||||
}
|
||||
expect(barNode.sourceEdge.name).to.eq('bar')
|
||||
|
||||
const fooNode = barNode.sourceEdge.source
|
||||
if (!fooNode) {
|
||||
expect.fail('should not fail')
|
||||
return
|
||||
}
|
||||
expect(fooNode.sourceEdge.name).to.eq('foo')
|
||||
});
|
||||
});
|
||||
37
backend/src/index.ts
Normal file
37
backend/src/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { TopicProperties, Tree, TreeNodeFactory } from './Model'
|
||||
import { MqttSource, DataSource } from './DataSource'
|
||||
|
||||
import * as socketIO from 'socket.io'
|
||||
|
||||
const server = require('http').createServer();
|
||||
|
||||
let tree = new Tree()
|
||||
let options = {url: 'mqtt://nodered'}
|
||||
let dataSource = new MqttSource()
|
||||
let count = 200
|
||||
|
||||
const a: Array<any> = []
|
||||
|
||||
const io = socketIO(server)
|
||||
io.on('connection', client => {
|
||||
console.log('connection')
|
||||
a.forEach(b => {
|
||||
io.emit('message', b)
|
||||
})
|
||||
client.on('event', data => { /* … */ });
|
||||
client.on('disconnect', () => { /* … */ });
|
||||
});
|
||||
server.listen(3000);
|
||||
|
||||
let state = dataSource.connect(options)
|
||||
dataSource.onMessage((topic: string, payload: Buffer) => {
|
||||
if (payload.length > 10000) {
|
||||
payload = payload.slice(0, 10000)
|
||||
}
|
||||
|
||||
io.emit('message', { topic, payload: payload.toString('base64') })
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
dataSource.disconnect()
|
||||
}, 1000000)
|
||||
Reference in New Issue
Block a user