├── .docker ├── golang │ └── Dockerfile ├── kafka │ ├── Dockerfile │ ├── bin │ │ └── setup.sh │ └── config │ │ ├── connect-standalone.properties │ │ └── server.properties ├── postgres │ ├── Dockerfile │ └── conf │ │ └── pg_hba.conf └── redis │ ├── Dockerfile │ └── conf │ └── redis.conf ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── services ├── api │ ├── .env.example │ ├── .gitignore │ ├── cmd │ │ └── main.go │ ├── config │ │ └── config.go │ ├── entity │ │ ├── auth.go │ │ └── command.go │ ├── go.mod │ ├── go.sum │ ├── handler │ │ ├── auth.go │ │ ├── command.go │ │ └── handler.go │ ├── internal │ │ └── context.go │ ├── middlewares │ │ └── auth.go │ ├── proto │ │ └── auth.proto │ ├── repository │ │ ├── auth.go │ │ └── command.go │ ├── server │ │ └── http.go │ └── storage │ │ ├── kafka.go │ │ ├── postgres.go │ │ └── storage.go ├── auth │ ├── .env.example │ ├── .gitignore │ ├── cmd │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── internal │ │ └── common.go │ ├── jwt │ │ └── jwt.go │ ├── proto │ │ └── auth.proto │ ├── repository │ │ └── auth.go │ ├── server │ │ └── server.go │ ├── storage │ │ ├── pg.go │ │ ├── redis.go │ │ └── storage.go │ └── user │ │ └── user.go ├── bot │ ├── bot │ │ ├── bot.go │ │ ├── message.go │ │ └── message_parser.go │ ├── cmd │ │ └── main.go │ ├── command │ │ ├── blacklist.go │ │ ├── command.go │ │ ├── command_type.go │ │ ├── consts.go │ │ └── helper.go │ ├── conn │ │ ├── irc.go │ │ └── irc_test.go │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── context.go │ │ └── context_test.go │ ├── test │ │ └── test.go │ └── utils │ │ └── write.go ├── message-reader │ ├── .env.example │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── app │ │ ├── main.go │ │ └── main2.go │ ├── client │ │ └── client.go │ ├── command │ │ ├── command.go │ │ └── command_test.go │ ├── conn │ │ └── irc.go │ ├── go.mod │ ├── go.sum │ ├── helpers │ │ ├── slice.go │ │ └── slice_test.go │ ├── internal │ │ └── context.go │ ├── message │ │ ├── message.go │ │ └── parser.go │ ├── proto │ │ ├── message.pb.go │ │ ├── message.proto │ │ └── message_grpc.pb.go │ ├── reader │ │ └── reader.go │ ├── repository │ │ └── message.go │ ├── server │ │ └── server.go │ ├── storage │ │ ├── redis.go │ │ └── storage.go │ ├── test │ │ └── test.go │ └── tmp │ │ └── .gitkeep ├── message-sender │ ├── .env.example │ ├── .gitignore │ ├── Makefile │ ├── cmd │ │ └── main.go │ ├── conn │ │ └── irc.go │ ├── go.mod │ ├── go.sum │ ├── helpers │ │ ├── slice.go │ │ └── slice_test.go │ ├── internal │ │ └── context.go │ ├── proto │ │ ├── message.pb.go │ │ ├── message.proto │ │ └── message_grpc.pb.go │ ├── repository │ │ └── message.go │ ├── sender │ │ └── sender.go │ ├── server │ │ └── server.go │ ├── test │ │ └── test.go │ └── tmp │ │ └── .gitkeep └── queue-mgr │ ├── .env.example │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── main.rs └── test.sh /.docker/golang/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine AS build 2 | 3 | WORKDIR /go/src/app 4 | 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | 10 | COPY . . 11 | RUN CGO_ENABLED=0 GOOS=linux go build -o bin/app cmd/main.go 12 | 13 | FROM golang:1.17-alpine 14 | 15 | COPY --from=build /go/src/app/bin/app . 16 | 17 | EXPOSE 8070 18 | 19 | CMD [ "./app", "-env=false" ] 20 | -------------------------------------------------------------------------------- /.docker/kafka/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/kafka:latest 2 | 3 | WORKDIR / 4 | 5 | COPY config/ /config/ 6 | COPY bin/setup.sh . 7 | 8 | EXPOSE 9092 9 | 10 | CMD ["./setup.sh"] 11 | -------------------------------------------------------------------------------- /.docker/kafka/bin/setup.sh: -------------------------------------------------------------------------------- 1 | kafka-server-start.sh /config/server.properties 2 | 3 | kafka-topics.sh --create --topic quickstart-events --bootstrap-server 127.0.0.1:9092 4 | -------------------------------------------------------------------------------- /.docker/kafka/config/connect-standalone.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # These are defaults. This file just demonstrates how to override some settings. 17 | bootstrap.servers=localhost:9092 18 | 19 | # The converters specify the format of data in Kafka and how to translate it into Connect data. Every Connect user will 20 | # need to configure these based on the format they want their data in when loaded from or stored into Kafka 21 | key.converter=org.apache.kafka.connect.json.JsonConverter 22 | value.converter=org.apache.kafka.connect.json.JsonConverter 23 | # Converter-specific settings can be passed in by prefixing the Converter's setting with the converter we want to apply 24 | # it to 25 | key.converter.schemas.enable=true 26 | value.converter.schemas.enable=true 27 | 28 | offset.storage.file.filename=/tmp/connect.offsets 29 | # Flush much faster than normal, which is useful for testing/debugging 30 | offset.flush.interval.ms=10000 31 | 32 | # Set to a list of filesystem paths separated by commas (,) to enable class loading isolation for plugins 33 | # (connectors, converters, transformations). The list should consist of top level directories that include 34 | # any combination of: 35 | # a) directories immediately containing jars with plugins and their dependencies 36 | # b) uber-jars with plugins and their dependencies 37 | # c) directories immediately containing the package directory structure of classes of plugins and their dependencies 38 | # Note: symlinks will be followed to discover dependencies or plugins. 39 | # Examples: 40 | # plugin.path=/usr/local/share/java,/usr/local/share/kafka/plugins,/opt/connectors, 41 | #plugin.path= 42 | -------------------------------------------------------------------------------- /.docker/kafka/config/server.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # see kafka.server.KafkaConfig for additional details and defaults 17 | 18 | ############################# Server Basics ############################# 19 | 20 | # The id of the broker. This must be set to a unique integer for each broker. 21 | broker.id=0 22 | 23 | ############################# Socket Server Settings ############################# 24 | 25 | # The address the socket server listens on. It will get the value returned from 26 | # java.net.InetAddress.getCanonicalHostName() if not configured. 27 | # FORMAT: 28 | # listeners = listener_name://host_name:port 29 | # EXAMPLE: 30 | # listeners = PLAINTEXT://your.host.name:9092 31 | listeners=PLAINTEXT://127.0.0.1:9092 32 | 33 | # Hostname and port the broker will advertise to producers and consumers. If not set, 34 | # it uses the value for "listeners" if configured. Otherwise, it will use the value 35 | # returned from java.net.InetAddress.getCanonicalHostName(). 36 | advertised.listeners=PLAINTEXT://127.0.0.1:9092 37 | 38 | # Maps listener names to security protocols, the default is for them to be the same. See the config documentation for more details 39 | listener.security.protocol.map=PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL 40 | #listener.security.protocol.map=PLAINTEXT:PLAINTEXT 41 | 42 | # The number of threads that the server uses for receiving requests from the network and sending responses to the network 43 | num.network.threads=3 44 | 45 | # The number of threads that the server uses for processing requests, which may include disk I/O 46 | num.io.threads=8 47 | 48 | # The send buffer (SO_SNDBUF) used by the socket server 49 | socket.send.buffer.bytes=102400 50 | 51 | # The receive buffer (SO_RCVBUF) used by the socket server 52 | socket.receive.buffer.bytes=102400 53 | 54 | # The maximum size of a request that the socket server will accept (protection against OOM) 55 | socket.request.max.bytes=104857600 56 | 57 | 58 | ############################# Log Basics ############################# 59 | 60 | # A comma separated list of directories under which to store log files 61 | log.dirs=/tmp/kafka-logs 62 | 63 | # The default number of log partitions per topic. More partitions allow greater 64 | # parallelism for consumption, but this will also result in more files across 65 | # the brokers. 66 | num.partitions=1 67 | 68 | # The number of threads per data directory to be used for log recovery at startup and flushing at shutdown. 69 | # This value is recommended to be increased for installations with data dirs located in RAID array. 70 | num.recovery.threads.per.data.dir=1 71 | 72 | ############################# Internal Topic Settings ############################# 73 | # The replication factor for the group metadata internal topics "__consumer_offsets" and "__transaction_state" 74 | # For anything other than development testing, a value greater than 1 is recommended to ensure availability such as 3. 75 | offsets.topic.replication.factor=1 76 | transaction.state.log.replication.factor=1 77 | transaction.state.log.min.isr=1 78 | 79 | ############################# Log Flush Policy ############################# 80 | 81 | # Messages are immediately written to the filesystem but by default we only fsync() to sync 82 | # the OS cache lazily. The following configurations control the flush of data to disk. 83 | # There are a few important trade-offs here: 84 | # 1. Durability: Unflushed data may be lost if you are not using replication. 85 | # 2. Latency: Very large flush intervals may lead to latency spikes when the flush does occur as there will be a lot of data to flush. 86 | # 3. Throughput: The flush is generally the most expensive operation, and a small flush interval may lead to excessive seeks. 87 | # The settings below allow one to configure the flush policy to flush data after a period of time or 88 | # every N messages (or both). This can be done globally and overridden on a per-topic basis. 89 | 90 | # The number of messages to accept before forcing a flush of data to disk 91 | #log.flush.interval.messages=10000 92 | 93 | # The maximum amount of time a message can sit in a log before we force a flush 94 | #log.flush.interval.ms=1000 95 | 96 | ############################# Log Retention Policy ############################# 97 | 98 | # The following configurations control the disposal of log segments. The policy can 99 | # be set to delete segments after a period of time, or after a given size has accumulated. 100 | # A segment will be deleted whenever *either* of these criteria are met. Deletion always happens 101 | # from the end of the log. 102 | 103 | # The minimum age of a log file to be eligible for deletion due to age 104 | log.retention.hours=168 105 | 106 | # A size-based retention policy for logs. Segments are pruned from the log unless the remaining 107 | # segments drop below log.retention.bytes. Functions independently of log.retention.hours. 108 | #log.retention.bytes=1073741824 109 | 110 | # The maximum size of a log segment file. When this size is reached a new log segment will be created. 111 | log.segment.bytes=1073741824 112 | 113 | # The interval at which log segments are checked to see if they can be deleted according 114 | # to the retention policies 115 | log.retention.check.interval.ms=300000 116 | 117 | ############################# Zookeeper ############################# 118 | 119 | # Zookeeper connection string (see zookeeper docs for details). 120 | # This is a comma separated host:port pairs, each corresponding to a zk 121 | # server. e.g. "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002". 122 | # You can also append an optional chroot string to the urls to specify the 123 | # root directory for all kafka znodes. 124 | zookeeper.connect=zookeeper:2181 125 | 126 | # Timeout in ms for connecting to zookeeper 127 | zookeeper.connection.timeout.ms=18000 128 | 129 | 130 | ############################# Group Coordinator Settings ############################# 131 | 132 | # The following configuration specifies the time, in milliseconds, that the GroupCoordinator will delay the initial consumer rebalance. 133 | # The rebalance will be further delayed by the value of group.initial.rebalance.delay.ms as new members join the group, up to a maximum of max.poll.interval.ms. 134 | # The default value for this is 3 seconds. 135 | # We override this to 0 here as it makes for a better out-of-the-box experience for development and testing. 136 | # However, in production environments the default value of 3 seconds is more suitable as this will help to avoid unnecessary, and potentially expensive, rebalances during application startup. 137 | group.initial.rebalance.delay.ms=0 138 | -------------------------------------------------------------------------------- /.docker/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:13-alpine 2 | 3 | COPY /.docker/postgres/conf/pg_hba.conf /var/lib/postgresql/data/pg_hba.conf 4 | 5 | CMD ["postgres"] 6 | 7 | EXPOSE 5432 8 | -------------------------------------------------------------------------------- /.docker/postgres/conf/pg_hba.conf: -------------------------------------------------------------------------------- 1 | # DB user address auth-method 2 | host all all 0.0.0.0/0 md5 -------------------------------------------------------------------------------- /.docker/redis/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM redis:6-buster 2 | 3 | COPY /.docker/redis/conf/redis.conf /usr/local/etc/redis/redis.conf 4 | 5 | CMD ["redis-server", "/usr/local/etc/redis/redis.conf"] 6 | 7 | EXPOSE 6379 -------------------------------------------------------------------------------- /.docker/redis/conf/redis.conf: -------------------------------------------------------------------------------- 1 | # Following https://raw.githubusercontent.com/redis/redis/6.0/redis.conf 2 | bind 0.0.0.0 3 | 4 | protected-mode no -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # IRC 2 | IRC_URL=irc.chat.twitch.tv 3 | IRC_PORT=6667 4 | IRC_SSL_URL=irc.chat.twitch.tv 5 | IRC_SSL_PORT=6697 6 | 7 | # Config 8 | IS_DOCKER=true 9 | 10 | # API 11 | API_PORT=5000 12 | API_NAME= 13 | 14 | API_PGSQL_HOST=localhost 15 | API_PGSQL_PORT=5432 16 | API_PGSQL_USER=root 17 | API_PGSQL_PASSWORD=root 18 | API_PGSQL_DBNAME=project 19 | API_PGSQL_NAME=api-postgres 20 | 21 | # Kafka 22 | ZOOKEEPER_PORT=2181 23 | ZOOKEEPER_NAME=bot-zookeeper 24 | ZOOKEEPER_SERVICE=zookeeper 25 | 26 | KAFKA_NAME=bot-kafka 27 | KAFKA_URL=localhost 28 | KAFKA_PORT=9092 29 | KAFKA_TOPICS=commands 30 | 31 | # Message Reader 32 | READER_PORT=5000 33 | READER_NAME=reader 34 | 35 | READER_BOT_OAUTH_TOKEN=rafiuskybot 36 | READER_BOT_USERNAME= 37 | 38 | READER_REDIS_HOST=localhost 39 | READER_REDIS_PORT=6379 40 | READER_REDIS_NAME=reader-redis 41 | READER_REDIS_DB=0 42 | READER_REDIS_PASSWORD= 43 | 44 | # Sender 45 | SENDER_PORT=5000 46 | SENDER_NAME= 47 | 48 | SENDER_BOT_OAUTH_TOKEN=rafiuskybot 49 | SENDER_BOT_USERNAME= 50 | 51 | # Auth 52 | AUTH_PORT=5000 53 | AUTH_NAME=auth 54 | 55 | AUTH_REDIS_HOST=localhost 56 | AUTH_REDIS_PORT=6379 57 | AUTH_REDIS_NAME=bot-redis 58 | AUTH_REDIS_DB=0 59 | AUTH_REDIS_PASSWORD= 60 | 61 | AUTH_PGSQL_HOST=localhost 62 | AUTH_PGSQL_PORT=5432 63 | AUTH_PGSQL_USER=root 64 | AUTH_PGSQL_PASSWORD=root 65 | AUTH_PGSQL_DBNAME=project 66 | AUTH_PGSQL_NAME=bot-postgres 67 | 68 | # Queue Manager 69 | QUEUE_MGR_PORT=6000 70 | QUEUE_MGR_NAME=queue-mgr 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .nvimlog 3 | 4 | .idea/ 5 | db/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rafaelbreno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-bot 2 | - Simple twitch bot made with Go - Study purpose 3 | - I'm following the official [Twitch Chatbot Docs](https://dev.twitch.tv/docs/irc/guide) 4 | - [AuthToken](https://twitchapps.com/tmi/) 5 | - [Application Architecture](https://whimsical.com/gobot-UhQLa6aXBkAXd4tSmJn5EZ) 6 | 7 | ### Deploy 8 | - Docker 9 | 10 | ### TODOv1: 11 | - [x] Stablish a Bot 12 | - [x] IRC Connection 13 | - [X] Commands 14 | - [x] Database (JSON) 15 | 16 | ### TODOv2: 17 | - [x] Develop API 18 | - [x] Establish API base 19 | - [x] Context 20 | - [x] Logger (uber-zap) 21 | - [x] WebFramework (fiber) 22 | - [x] ORM/DB Connection (GORM) 23 | - [x] Define DB 24 | - [x] SQL - PostgreSQL 25 | - [x] Write Migrations 26 | - [x] Define Entities 27 | 28 | ### TODOv3: 29 | - [x] Study the difference between: 30 | - [x] RabbitMQ and Kafka 31 | - [x] HTTP and gRPC 32 | - [x] Up Kafka using Docker 33 | - [x] API 34 | - [x] Implement the communication between API and Kafka/RabbitMQ 35 | - [x] Produce - Kafka 36 | 37 | ### TODOv4: 38 | - [x] Queue Manager 39 | - [x] Create microservice 40 | - [x] Implement the communication between API and Kafka/RabbitMQ 41 | - [x] Implement Redis 42 | - [x] Consume - RabbitMQ/Kafka 43 | - [x] Insert/Update/Delete Redis rows 44 | 45 | ### TODOv5 46 | - [ ] Auth Service 47 | - [ ] Implement Redis 48 | - [ ] API 49 | - [ ] Communicate with Auth Service 50 | - [ ] Add middleware 51 | - [x] Update Migrations 52 | 53 | ### TODO(v?): 54 | - [ ] Local deploy Docker 55 | - [ ] Front-end 56 | - [ ] Choose one 57 | - [ ] React (with Rescript) 58 | - [ ] Vue 59 | - [ ] Svelte <- more likely 60 | - [ ] OAuth 61 | - [ ] Prometheus 62 | - [ ] noSQL - Redis 63 | - [ ] Define an Entity 64 | - [x] Define key layout 65 | - [stream][user] 66 | - [ ] Implement HealthCheck 67 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: 6 | context: services/api/ 7 | dockerfile: ../../.docker/golang/Dockerfile 8 | ports: 9 | - "${API_PORT}:${API_PORT}" 10 | image: "${API_NAME}" 11 | container_name: "${API_NAME}" 12 | environment: 13 | - PGSQL_HOST=${API_PGSQL_HOST} 14 | - PGSQL_PORT=${API_PGSQL_PORT} 15 | - PGSQL_USER=${API_PGSQL_USER} 16 | - PGSQL_PASSWORD=${API_PGSQL_PASSWORD} 17 | - PGSQL_DBNAME=${API_PGSQL_DBNAME} 18 | - PGSQL_NAME=${API_PGSQL_NAME} 19 | - AUTH_HOST=${AUTH_NAME} 20 | - AUTH_PORT=${AUTH_PORT} 21 | api-postgres: 22 | build: 23 | context: . 24 | dockerfile: .docker/postgres/Dockerfile 25 | ports: 26 | - "${API_PGSQL_PORT}:${API_PGSQL_PORT}" 27 | image: ${API_PGSQL_NAME} 28 | container_name: ${API_PGSQL_NAME} 29 | environment: 30 | - PGDATA=${API_PGSQL_DATA} 31 | - POSTGRES_DB=${API_PGSQL_DBNAME} 32 | - POSTGRES_USER=${API_PGSQL_USER} 33 | - POSTGRES_PASSWORD=${API_PGSQL_PASSWORD} 34 | auth: 35 | build: 36 | context: services/auth/ 37 | dockerfile: ../../.docker/golang/Dockerfile 38 | ports: 39 | - "${AUTH_PORT}:${AUTH_PORT}" 40 | image: "${AUTH_NAME}" 41 | container_name: "${AUTH_NAME}" 42 | environment: 43 | - AUTH_PORT=${AUTH_PORT} 44 | - REDIS_HOST=${AUTH_REDIS_HOST} 45 | - REDIS_PORT=${AUTH_REDIS_PORT} 46 | - REDIS_NAME=${AUTH_REDIS_NAME} 47 | - REDIS_DB=${AUTH_REDIS_DB} 48 | - REDIS_PASSWORD=${AUTH_REDIS_PASSWORD} 49 | - PGSQL_HOST=${AUTH_PGSQL_HOST} 50 | - PGSQL_PORT=${AUTH_PGSQL_PORT} 51 | - PGSQL_USER=${AUTH_PGSQL_USER} 52 | - PGSQL_PASSWORD=${AUTH_PGSQL_PASSWORD} 53 | - PGSQL_DBNAME=${AUTH_PGSQL_DBNAME} 54 | - PGSQL_NAME=${AUTH_PGSQL_NAME} 55 | auth-postgres: 56 | build: 57 | context: . 58 | dockerfile: .docker/postgres/Dockerfile 59 | ports: 60 | - "${AUTH_PGSQL_PORT}:${AUTH_PGSQL_PORT}" 61 | image: ${AUTH_PGSQL_NAME} 62 | container_name: ${AUTH_PGSQL_NAME} 63 | environment: 64 | - PGDATA=${AUTH_PGSQL_DATA} 65 | - POSTGRES_DB=${AUTH_PGSQL_DBNAME} 66 | - POSTGRES_USER=${AUTH_PGSQL_USER} 67 | - POSTGRES_PASSWORD=${AUTH_PGSQL_PASSWORD} 68 | auth-redis: 69 | build: 70 | context: . 71 | dockerfile: .docker/redis/Dockerfile 72 | ports: 73 | - "${AUTH_REDIS_PORT}:6379" 74 | image: ${AUTH_REDIS_NAME} 75 | container_name: ${AUTH_REDIS_NAME} 76 | reader: 77 | build: 78 | context: services/reader/ 79 | dockerfile: ../../.docker/golang/Dockerfile 80 | ports: 81 | - "${READER_PORT}:${READER_PORT}" 82 | image: "${READER_NAME}" 83 | container_name: "${READER_NAME}" 84 | environment: 85 | - CHANNEL_NAME=${READER_CHANNEL_NAME} 86 | - BOT_OAUTH_TOKEN=${READER_BOT_OAUTH_TOKEN} 87 | - BOT_USERNAME=${READER_BOT_USERNAME} 88 | - IRC_URL=${READER_IRC_URL} 89 | - IRC_PORT=${READER_IRC_PORT} 90 | - REDIS_HOST=${READER_REDIS_HOST} 91 | - REDIS_PORT=${READER_REDIS_PORT} 92 | - REDIS_NAME=${READER_REDIS_NAME} 93 | - REDIS_DB=${READER_REDIS_DB} 94 | - REDIS_PASSWORD=${READER_REDIS_PASSWORD} 95 | reader-redis: 96 | build: 97 | context: . 98 | dockerfile: .docker/redis/Dockerfile 99 | ports: 100 | - "${READER_REDIS_PORT}:6379" 101 | image: ${READER_REDIS_NAME} 102 | container_name: ${READER_REDIS_NAME} 103 | sender: 104 | build: 105 | context: services/message-sender/ 106 | dockerfile: ../../.docker/golang/Dockerfile 107 | ports: 108 | - "${SENDER_PORT}:${SENDER_PORT}" 109 | image: "${SENDER_NAME}" 110 | container_name: "${SENDER_NAME}" 111 | environment: 112 | - CHANNEL_NAME=${SENDER_CHANNEL_NAME} 113 | - BOT_OAUTH_TOKEN=${SENDER_BOT_OAUTH_TOKEN} 114 | - BOT_USERNAME=${SENDER_BOT_USERNAME} 115 | - IRC_URL=${SENDER_IRC_URL} 116 | - IRC_PORT=${SENDER_IRC_PORT} 117 | queue-mgr: 118 | build: 119 | context: services/queue-mgr/ 120 | dockerfile: ../../.docker/golang/Dockerfile 121 | ports: 122 | - "${QUEUE_MGR_PORT}:${QUEUE_MGR_PORT}" 123 | image: "${QUEUE_MGR_NAME}" 124 | container_name: "${QUEUE_MGR_NAME}" 125 | environment: 126 | - REDIS_HOST=${READER_REDIS_HOST} 127 | - REDIS_PORT=${READER_REDIS_PORT} 128 | - REDIS_NAME=${READER_REDIS_NAME} 129 | - REDIS_DB=${READER_REDIS_DB} 130 | - REDIS_PASSWORD=${READER_REDIS_PASSWORD} 131 | - KAFKA_URL=${KAFKA_URL} 132 | - KAFKA_PORT=${KAFKA_PORT} 133 | - KAFKA_TOPIC=${KAFKA_TOPIC} 134 | zookeeper: 135 | image: wurstmeister/zookeeper:3.4.6 136 | container_name: "${ZOOKEEPER_NAME}" 137 | ports: 138 | - "${ZOOKEEPER_PORT}:${ZOOKEEPER_PORT}" 139 | kafka: 140 | image: wurstmeister/kafka:2.13-2.7.0 141 | container_name: "${KAFKA_NAME}" 142 | ports: 143 | - "${KAFKA_PORT}:${KAFKA_PORT}" 144 | links: 145 | - zookeeper 146 | environment: 147 | KAFKA_ZOOKEEPER_CONNECT: "${ZOOKEEPER_SERVICE}:${ZOOKEEPER_PORT}" 148 | KAFKA_ADVERTISED_HOST_NAME: "localhost" 149 | KAFKA_ADVERTISED_PORT: "${KAFKA_PORT}" 150 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 151 | depends_on: 152 | - zookeeper 153 | -------------------------------------------------------------------------------- /services/api/.env.example: -------------------------------------------------------------------------------- 1 | API_PORT=5000 2 | 3 | PGSQL_DATA=db 4 | PGSQL_HOST=localhost 5 | PGSQL_PORT=5432 6 | PGSQL_USER=root 7 | PGSQL_PASSWORD=root 8 | PGSQL_DBNAME=project 9 | PGSQL_NAME=bot-postgres 10 | 11 | REDIS_HOST=localhost 12 | REDIS_PORT=6379 13 | REDIS_NAME=bot-redis 14 | 15 | AUTH_URL=localhost 16 | AUTH_port=8000 17 | AUTH_ENDPOINT=/api/profile 18 | 19 | KAFKA_URL=localhost 20 | KAFKA_PORT=9092 21 | KAFKA_TOPIC=commands 22 | -------------------------------------------------------------------------------- /services/api/.gitignore: -------------------------------------------------------------------------------- 1 | proto/*.go 2 | -------------------------------------------------------------------------------- /services/api/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rafaelbreno/go-bot/api/config" 7 | "github.com/rafaelbreno/go-bot/api/entity" 8 | "github.com/rafaelbreno/go-bot/api/internal" 9 | "github.com/rafaelbreno/go-bot/api/server" 10 | ) 11 | 12 | var ctx *internal.Context 13 | var sv *server.Server 14 | 15 | func init() { 16 | ctx = config.Ctx 17 | sv = server.NewServer() 18 | } 19 | 20 | func main() { 21 | config.Storage.SQL.Client.AutoMigrate(&entity.Command{}) 22 | 23 | ctx.Logger.Info("Starting app") 24 | 25 | sv.ListenAndServe() 26 | 27 | defer sv.Close() 28 | 29 | st := config.Storage 30 | 31 | defer st.KafkaClient.P.Close() 32 | db, _ := st.SQL.Client.DB() 33 | defer func() { 34 | if err := db.Close(); err != nil { 35 | ctx.Logger.Error(err.Error()) 36 | os.Exit(0) 37 | } 38 | }() 39 | ctx.Logger.Info("Gracefully terminating...") 40 | } 41 | -------------------------------------------------------------------------------- /services/api/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | "github.com/rafaelbreno/go-bot/api/internal" 9 | "github.com/rafaelbreno/go-bot/api/storage" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var ( 14 | // Storage variable 15 | Storage *storage.Storage 16 | 17 | // Ctx - responsible for storing 18 | // Logger and env variables 19 | Ctx *internal.Context 20 | ) 21 | 22 | func init() { 23 | if err := godotenv.Load(); err != nil { 24 | fmt.Println(err.Error()) 25 | os.Exit(0) 26 | } 27 | 28 | l, err := zap.NewProduction() 29 | if err != nil { 30 | fmt.Println(err.Error()) 31 | os.Exit(0) 32 | } 33 | 34 | Ctx = &internal.Context{ 35 | Logger: l, 36 | Env: map[string]string{ 37 | "PGSQL_HOST": os.Getenv("PGSQL_HOST"), 38 | "PGSQL_PORT": os.Getenv("PGSQL_PORT"), 39 | "PGSQL_USER": os.Getenv("PGSQL_USER"), 40 | "PGSQL_PASSWORD": os.Getenv("PGSQL_PASSWORD"), 41 | "PGSQL_DBNAME": os.Getenv("PGSQL_DBNAME"), 42 | "REDIS_HOST": os.Getenv("REDIS_HOST"), 43 | "REDIS_PORT": os.Getenv("REDIS_PORT"), 44 | "REDIS_PASSWORD": os.Getenv("REDIS_PASSWORD"), 45 | "KAFKA_URL": os.Getenv("KAFKA_URL"), 46 | "KAFKA_PORT": os.Getenv("KAFKA_PORT"), 47 | "KAFKA_TOPIC": os.Getenv("KAFKA_TOPIC"), 48 | "AUTH_URL": os.Getenv("AUTH_URL"), 49 | "AUTH_PORT": os.Getenv("AUTH_PORT"), 50 | "AUTH_ENDPOINT": os.Getenv("AUTH_ENDPOINT"), 51 | "API_PORT": getPort(), 52 | }, 53 | } 54 | 55 | Storage = storage.NewStorage(Ctx) 56 | } 57 | 58 | func getPort() string { 59 | envPort := os.Getenv("API_PORT") 60 | if []byte(envPort)[0] == ':' { 61 | return envPort 62 | } 63 | 64 | return string(append([]byte(":"), []byte(envPort)...)) 65 | } 66 | -------------------------------------------------------------------------------- /services/api/entity/auth.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type User struct { 8 | ID uuid.UUID `json:"id"` 9 | Username string `json:"username"` 10 | Password string `json:"password"` 11 | PasswordConfirmation string `json:"password_confirmation"` 12 | Token string `json:"token"` 13 | } 14 | 15 | type UserResponse struct { 16 | Token string `json:"token"` 17 | Error string `json:"error"` 18 | } 19 | -------------------------------------------------------------------------------- /services/api/entity/command.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // CommandType define what kinda of parsing 12 | // will be executed at one command. 13 | type CommandType string 14 | 15 | const ( 16 | // Common commands, have a trigger 17 | // and an answer. 18 | Common CommandType = "COMMON" 19 | // Repeat commands, are those which 20 | // will be executed every X period of time. 21 | Repeat CommandType = "REPEAT" 22 | ) 23 | 24 | // Command stores data related. 25 | type Command struct { 26 | ID uuid.UUID `json:"id" gorm:"type:uuid;default:gen_random_uuid()"` 27 | UserID uuid.UUID `json:"user_id" gorm:"type:uuid"` 28 | Trigger string `json:"trigger" gorm:"size:16"` 29 | Template string `json:"template" gorm:"size:400"` 30 | Cooldown string `json:"cooldown" gorm:"size:16"` 31 | CommandType string `json:"command_type" gorm:"type:varchar;size:16"` 32 | CreatedAt time.Time 33 | UpdatedAt time.Time 34 | DeletedAt gorm.DeletedAt `gorm:"index"` 35 | } 36 | 37 | // ToJSON convert Command struct to 38 | // a map type 39 | func (c Command) ToJSON() CommandJSON { 40 | return CommandJSON{ 41 | ID: c.ID, 42 | UserID: c.UserID, 43 | Trigger: c.Trigger, 44 | Template: c.Template, 45 | CommandType: c.CommandType, 46 | Cooldown: c.Cooldown, 47 | } 48 | } 49 | 50 | func (c *Command) UpdateFields(cmdFields Command) { 51 | if c.Trigger != cmdFields.Trigger { 52 | c.Trigger = cmdFields.Trigger 53 | } 54 | if c.Template != cmdFields.Template { 55 | c.Template = cmdFields.Template 56 | } 57 | if c.Cooldown != cmdFields.Cooldown { 58 | c.Cooldown = cmdFields.Cooldown 59 | } 60 | if c.CommandType != cmdFields.CommandType { 61 | c.CommandType = cmdFields.CommandType 62 | } 63 | } 64 | 65 | // CommandJSON DTO to receive data from 66 | // http request 67 | type CommandJSON struct { 68 | ID uuid.UUID `json:"id"` 69 | UserID uuid.UUID `json:"user_id"` 70 | Trigger string `json:"trigger"` 71 | Template string `json:"template"` 72 | Cooldown string `json:"cooldown"` 73 | CommandType string `json:"command_type"` 74 | } 75 | 76 | func (c *CommandJSON) ToCommand() Command { 77 | return Command{ 78 | ID: c.ID, 79 | UserID: c.UserID, 80 | Trigger: c.Trigger, 81 | Template: c.Template, 82 | CommandType: c.CommandType, 83 | Cooldown: c.Cooldown, 84 | } 85 | } 86 | 87 | // ToJSONString returns CommandJSON as JSON string 88 | func (c *CommandJSON) ToJSONString() ([]byte, error) { 89 | b, err := json.Marshal(c) 90 | 91 | return b, err 92 | } 93 | -------------------------------------------------------------------------------- /services/api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rafaelbreno/go-bot/api 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.18.0 7 | github.com/google/uuid v1.3.0 8 | github.com/joho/godotenv v1.3.0 9 | go.uber.org/zap v1.19.0 10 | google.golang.org/grpc v1.26.0 11 | google.golang.org/protobuf v1.27.1 12 | gopkg.in/confluentinc/confluent-kafka-go.v1 v1.7.0 13 | gorm.io/driver/postgres v1.1.0 14 | gorm.io/gorm v1.21.14 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.3 // indirect 19 | github.com/confluentinc/confluent-kafka-go v1.7.0 // indirect 20 | github.com/golang/protobuf v1.5.0 // indirect 21 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 22 | github.com/jackc/pgconn v1.10.0 // indirect 23 | github.com/jackc/pgio v1.0.0 // indirect 24 | github.com/jackc/pgpassfile v1.0.0 // indirect 25 | github.com/jackc/pgproto3/v2 v2.1.1 // indirect 26 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 27 | github.com/jackc/pgtype v1.8.1 // indirect 28 | github.com/jackc/pgx/v4 v4.13.0 // indirect 29 | github.com/jinzhu/inflection v1.0.0 // indirect 30 | github.com/jinzhu/now v1.1.2 // indirect 31 | github.com/klauspost/compress v1.13.4 // indirect 32 | github.com/valyala/bytebufferpool v1.0.0 // indirect 33 | github.com/valyala/fasthttp v1.29.0 // indirect 34 | github.com/valyala/tcplisten v1.0.0 // indirect 35 | go.uber.org/atomic v1.9.0 // indirect 36 | go.uber.org/multierr v1.7.0 // indirect 37 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 38 | golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect 39 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 40 | golang.org/x/text v0.3.6 // indirect 41 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e // indirect 42 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect 43 | gopkg.in/yaml.v2 v2.4.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /services/api/handler/auth.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/rafaelbreno/go-bot/api/entity" 8 | "github.com/rafaelbreno/go-bot/api/internal" 9 | "github.com/rafaelbreno/go-bot/api/repository" 10 | ) 11 | 12 | type AuthHandler struct { 13 | repo *repository.AuthRepoCtx 14 | } 15 | 16 | func NewUserHandler(ctx *internal.Context) AuthHandler { 17 | return AuthHandler{repository.NewAuthRepoCtx(ctx)} 18 | } 19 | 20 | func (a *AuthHandler) Create(c *fiber.Ctx) error { 21 | userJSON := new(entity.User) 22 | 23 | if len(c.Body()) <= 0 { 24 | return c. 25 | Status(http.StatusUnprocessableEntity). 26 | JSON(fiber.Map{ 27 | "error": "Empty body", 28 | }) 29 | } 30 | 31 | if err := c.BodyParser(userJSON); err != nil { 32 | a.repo.Ctx.Logger.Error(err.Error()) 33 | return c. 34 | Status(http.StatusUnprocessableEntity). 35 | JSON(fiber.Map{ 36 | "error": err.Error(), 37 | }) 38 | } 39 | 40 | authRes, err := a.repo.Create(*userJSON) 41 | 42 | if err != nil { 43 | a.repo.Ctx.Logger.Error(err.Error()) 44 | return c. 45 | Status(http.StatusUnprocessableEntity). 46 | JSON(fiber.Map{ 47 | "error": err.Error(), 48 | }) 49 | } 50 | return c. 51 | Status(http.StatusCreated). 52 | JSON(authRes) 53 | } 54 | 55 | func (a *AuthHandler) Login(c *fiber.Ctx) error { 56 | userJSON := new(entity.User) 57 | 58 | if len(c.Body()) <= 0 { 59 | return c. 60 | Status(http.StatusUnprocessableEntity). 61 | JSON(fiber.Map{ 62 | "error": "Empty body", 63 | }) 64 | } 65 | 66 | if err := c.BodyParser(userJSON); err != nil { 67 | a.repo.Ctx.Logger.Error(err.Error()) 68 | return c. 69 | Status(http.StatusUnprocessableEntity). 70 | JSON(fiber.Map{ 71 | "error": err.Error(), 72 | }) 73 | } 74 | 75 | authRes, err := a.repo.Login(*userJSON) 76 | 77 | if err != nil { 78 | a.repo.Ctx.Logger.Error(err.Error()) 79 | return c. 80 | Status(http.StatusUnprocessableEntity). 81 | JSON(fiber.Map{ 82 | "error": err.Error(), 83 | }) 84 | } 85 | return c. 86 | Status(http.StatusCreated). 87 | JSON(authRes) 88 | } 89 | 90 | func (a *AuthHandler) Check(c *fiber.Ctx) error { 91 | userJSON := new(entity.User) 92 | 93 | if len(c.Body()) <= 0 { 94 | return c. 95 | Status(http.StatusUnprocessableEntity). 96 | JSON(fiber.Map{ 97 | "error": "Empty body", 98 | }) 99 | } 100 | 101 | if err := c.BodyParser(userJSON); err != nil { 102 | a.repo.Ctx.Logger.Error(err.Error()) 103 | return c. 104 | Status(http.StatusUnprocessableEntity). 105 | JSON(fiber.Map{ 106 | "error": err.Error(), 107 | }) 108 | } 109 | 110 | err := a.repo.Check(userJSON.Token) 111 | 112 | if err != nil { 113 | a.repo.Ctx.Logger.Error(err.Error()) 114 | return c. 115 | Status(http.StatusUnprocessableEntity). 116 | JSON(fiber.Map{ 117 | "error": err.Error(), 118 | }) 119 | } 120 | return c. 121 | Status(http.StatusCreated). 122 | JSON(fiber.Map{ 123 | "message": "ok", 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /services/api/handler/command.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/google/uuid" 8 | "github.com/rafaelbreno/go-bot/api/entity" 9 | "github.com/rafaelbreno/go-bot/api/repository" 10 | ) 11 | 12 | // CommandHandler manages all endpoints 13 | // related to Command entity. 14 | type CommandHandler struct { 15 | repo repository.CommandRepoCtx 16 | } 17 | 18 | // NewCommandHandler created and returns 19 | // a configured CommandHandler. 20 | func NewCommandHandler() CommandHandler { 21 | return CommandHandler{repository.NewCommandRepoCtx()} 22 | } 23 | 24 | // Create - receive and creates a new command 25 | func (h *CommandHandler) Create(c *fiber.Ctx) error { 26 | commandJSON := new(entity.CommandJSON) 27 | 28 | if len(c.Body()) <= 0 { 29 | return c. 30 | Status(http.StatusUnprocessableEntity). 31 | JSON(fiber.Map{ 32 | "error": "Empty body", 33 | }) 34 | } 35 | 36 | if err := c.BodyParser(commandJSON); err != nil { 37 | h.repo.Ctx.Logger.Error(err.Error()) 38 | return c. 39 | Status(http.StatusUnprocessableEntity). 40 | JSON(fiber.Map{ 41 | "error": err.Error(), 42 | }) 43 | } 44 | 45 | commandJSON.UserID = uuid.MustParse(GetUserID(c)) 46 | 47 | command, err := h.repo.Create(commandJSON.ToCommand()) 48 | 49 | if err != nil { 50 | h.repo.Ctx.Logger.Error(err.Error()) 51 | return c. 52 | Status(http.StatusUnprocessableEntity). 53 | JSON(fiber.Map{ 54 | "error": err.Error(), 55 | }) 56 | } 57 | 58 | return c. 59 | Status(http.StatusCreated). 60 | JSON(command) 61 | } 62 | 63 | // Read - return a Command with given ID 64 | func (h *CommandHandler) Read(c *fiber.Ctx) error { 65 | command, err := h.repo.Read(c.Params("id"), GetUserID(c)) 66 | 67 | if err != nil { 68 | h.repo.Ctx.Logger.Error(err.Error()) 69 | return c. 70 | Status(http.StatusNotFound). 71 | JSON(fiber.Map{ 72 | "error": err.Error(), 73 | }) 74 | } 75 | 76 | return c. 77 | Status(http.StatusOK). 78 | JSON(command) 79 | } 80 | 81 | // Update - receive Command's fields and ID 82 | // to update it 83 | func (h *CommandHandler) Update(c *fiber.Ctx) error { 84 | if len(c.Body()) <= 0 { 85 | return c. 86 | Status(http.StatusUnprocessableEntity). 87 | JSON(fiber.Map{ 88 | "error": "Empty body", 89 | }) 90 | } 91 | commandJSON := new(entity.CommandJSON) 92 | 93 | if err := c.BodyParser(commandJSON); err != nil { 94 | h.repo.Ctx.Logger.Error(err.Error()) 95 | return c. 96 | Status(http.StatusUnprocessableEntity). 97 | JSON(fiber.Map{ 98 | "error": err.Error(), 99 | }) 100 | } 101 | commandJSON.UserID = uuid.MustParse(GetUserID(c)) 102 | 103 | command, err := h.repo.Update(c.Params("id"), commandJSON.ToCommand()) 104 | 105 | if err != nil { 106 | h.repo.Ctx.Logger.Error(err.Error()) 107 | return c. 108 | Status(http.StatusUnprocessableEntity). 109 | JSON(fiber.Map{ 110 | "error": err.Error(), 111 | }) 112 | } 113 | 114 | return c. 115 | Status(http.StatusOK). 116 | JSON(command) 117 | } 118 | 119 | // Delete - receive a id and delete the 120 | // command identified by it 121 | func (h *CommandHandler) Delete(c *fiber.Ctx) error { 122 | err := h.repo.Delete(c.Params("id"), GetUserID(c)) 123 | 124 | if err != nil { 125 | h.repo.Ctx.Logger.Error(err.Error()) 126 | return c. 127 | Status(http.StatusNotFound). 128 | JSON(fiber.Map{ 129 | "error": err.Error(), 130 | }) 131 | } 132 | 133 | return c. 134 | Status(http.StatusOK). 135 | JSON(fiber.Map{ 136 | "message": "Command deleted!", 137 | }) 138 | } 139 | 140 | func GetUserID(f *fiber.Ctx) string { 141 | return f.UserContext().Value("user_id").(string) 142 | } 143 | -------------------------------------------------------------------------------- /services/api/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/rafaelbreno/go-bot/api/internal" 6 | "github.com/rafaelbreno/go-bot/api/storage" 7 | ) 8 | 9 | // Handler handles http requests 10 | type Handler struct { 11 | Ctx *internal.Context 12 | Storage *storage.Storage 13 | } 14 | 15 | // Ping returns ok 16 | func (h Handler) Ping(c *fiber.Ctx) error { 17 | h.Ctx.Logger.Info("GET /ping") 18 | return c.JSON(fiber.Map{ 19 | "message": "ok", 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /services/api/internal/context.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | // Context is the internal structure 8 | // of this API 9 | type Context struct { 10 | Logger *zap.Logger 11 | Env map[string]string 12 | } 13 | -------------------------------------------------------------------------------- /services/api/middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/gofiber/fiber/v2" 11 | "github.com/rafaelbreno/go-bot/api/config" 12 | ) 13 | 14 | type userAPI struct { 15 | ID string `json:"id"` 16 | } 17 | 18 | // CheckAuth check if user is authenticated 19 | func CheckAuth(ctx *fiber.Ctx) error { 20 | appCtx := config.Ctx 21 | 22 | b := ctx.Get("Authorization") 23 | bearer := string(b) 24 | 25 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:%s%s", appCtx.Env["AUTH_URL"], appCtx.Env["AUTH_PORT"], appCtx.Env["AUTH_ENDPOINT"]), nil) 26 | 27 | if err != nil { 28 | return ctx. 29 | Status(http.StatusBadRequest). 30 | JSON(fiber.Map{ 31 | "error": err.Error(), 32 | }) 33 | } 34 | 35 | client := http.Client{} 36 | 37 | req.Header.Add("Authorization", bearer) 38 | req.Header.Add("Accept", "application/json") 39 | 40 | res, err := client.Do(req) 41 | 42 | if err != nil { 43 | return ctx. 44 | Status(http.StatusBadRequest). 45 | JSON(fiber.Map{ 46 | "error": err.Error(), 47 | }) 48 | } 49 | 50 | if res.StatusCode != http.StatusOK { 51 | return ctx. 52 | Status(res.StatusCode). 53 | JSON(fiber.Map{ 54 | "error": "unauthorized", 55 | }) 56 | } 57 | 58 | resBody := res.Body 59 | 60 | resBytes, _ := ioutil.ReadAll(resBody) 61 | 62 | var v userAPI 63 | 64 | if err := json.Unmarshal(resBytes, &v); err != nil { 65 | return ctx. 66 | Status(http.StatusBadRequest). 67 | JSON(fiber.Map{ 68 | "error": err.Error(), 69 | }) 70 | } 71 | 72 | userCtx := context.WithValue(ctx.UserContext(), "user_id", v.ID) 73 | 74 | ctx.SetUserContext(userCtx) 75 | 76 | return ctx.Next() 77 | } 78 | -------------------------------------------------------------------------------- /services/api/proto/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | option go_package = "/proto"; 5 | 6 | service Auth { 7 | rpc Create(CreateRequest) returns (CreateResponse) {} 8 | rpc Login(LoginRequest) returns (LoginResponse) {} 9 | rpc Check(CheckRequest) returns (CheckResponse) {} 10 | } 11 | 12 | message CreateRequest { 13 | string username = 1; 14 | string password = 2; 15 | string password_confirmation = 3; 16 | } 17 | 18 | message CreateResponse { 19 | string token = 1; 20 | string error = 2; 21 | } 22 | 23 | message LoginRequest { 24 | string username = 1; 25 | string password = 2; 26 | } 27 | 28 | message LoginResponse { 29 | string token = 1; 30 | string error = 2; 31 | } 32 | 33 | message CheckRequest { 34 | string token = 1; 35 | } 36 | 37 | message CheckResponse { 38 | string error = 1; 39 | } 40 | -------------------------------------------------------------------------------- /services/api/repository/auth.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/rafaelbreno/go-bot/api/entity" 9 | "github.com/rafaelbreno/go-bot/api/internal" 10 | "github.com/rafaelbreno/go-bot/api/proto" 11 | grpc "google.golang.org/grpc" 12 | ) 13 | 14 | type AuthRepo interface { 15 | Create(u entity.User) (entity.UserResponse, error) 16 | Login(u entity.User) (entity.UserResponse, error) 17 | Check(token string) error 18 | } 19 | 20 | type AuthRepoCtx struct { 21 | Ctx *internal.Context 22 | AuthClient proto.AuthClient 23 | } 24 | 25 | func NewAuthRepoCtx(ctx *internal.Context) *AuthRepoCtx { 26 | conn, err := grpc.Dial("localhost:5004", grpc.WithInsecure()) 27 | 28 | if err != nil { 29 | ctx.Logger.Error(err.Error()) 30 | return &AuthRepoCtx{} 31 | } 32 | 33 | client := proto.NewAuthClient(conn) 34 | 35 | return &AuthRepoCtx{ 36 | Ctx: ctx, 37 | AuthClient: client, 38 | } 39 | } 40 | 41 | func (a *AuthRepoCtx) Create(u entity.User) (entity.UserResponse, error) { 42 | createReq := &proto.CreateRequest{ 43 | Username: u.Username, 44 | Password: u.Password, 45 | PasswordConfirmation: u.PasswordConfirmation, 46 | } 47 | 48 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 49 | 50 | defer cancel() 51 | 52 | createRes, err := a.AuthClient.Create(ctx, createReq) 53 | 54 | if err != nil { 55 | return entity.UserResponse{ 56 | Error: err.Error(), 57 | }, err 58 | } 59 | 60 | userResp := entity.UserResponse{ 61 | Token: createRes.Token, 62 | Error: createRes.Error, 63 | } 64 | 65 | return userResp, nil 66 | } 67 | 68 | func (a *AuthRepoCtx) Login(u entity.User) (entity.UserResponse, error) { 69 | loginReq := &proto.LoginRequest{ 70 | Username: u.Username, 71 | Password: u.Password, 72 | } 73 | 74 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 75 | 76 | defer cancel() 77 | 78 | loginRes, err := a.AuthClient.Login(ctx, loginReq) 79 | 80 | if err != nil { 81 | return entity.UserResponse{ 82 | Error: err.Error(), 83 | }, err 84 | } 85 | 86 | userResp := entity.UserResponse{ 87 | Token: loginRes.Token, 88 | Error: loginRes.Error, 89 | } 90 | 91 | return userResp, nil 92 | } 93 | 94 | func (a *AuthRepoCtx) Check(token string) error { 95 | checkReq := &proto.CheckRequest{ 96 | Token: token, 97 | } 98 | 99 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 100 | 101 | defer cancel() 102 | 103 | checkRes, err := a.AuthClient.Check(ctx, checkReq) 104 | 105 | if err != nil { 106 | return err 107 | } 108 | 109 | if checkRes.Error != "" { 110 | return fmt.Errorf(checkRes.Error) 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /services/api/repository/command.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rafaelbreno/go-bot/api/config" 7 | "github.com/rafaelbreno/go-bot/api/entity" 8 | "github.com/rafaelbreno/go-bot/api/internal" 9 | "github.com/rafaelbreno/go-bot/api/storage" 10 | ) 11 | 12 | // CommandRepo stores the functions 13 | // related to Command actions. 14 | type CommandRepo interface { 15 | Create(cmd entity.Command) (entity.Command, error) 16 | Update(id string, cmd entity.Command) (entity.Command, error) 17 | Read(id, user_id string) (entity.Command, error) 18 | Delete(id, user_id string) error 19 | } 20 | 21 | // CommandRepoCtx represents 22 | // the Command's context. 23 | type CommandRepoCtx struct { 24 | Storage *storage.Storage 25 | Ctx *internal.Context 26 | } 27 | 28 | const ( 29 | add = "ADD" 30 | update = "UPDATE" 31 | remove = "DELETE" 32 | ) 33 | 34 | // Create insert a new command into 35 | // the database. 36 | func (cr CommandRepoCtx) Create(cmd entity.Command) (entity.Command, error) { 37 | if err := cr.Storage.SQL.Client.Create(&cmd).Error; err != nil { 38 | cr.Storage.Ctx.Logger.Error(err.Error()) 39 | return entity.Command{}, err 40 | } 41 | 42 | jsonCommand := cmd.ToJSON() 43 | 44 | b, err := jsonCommand.ToJSONString() 45 | 46 | if err != nil { 47 | cr.Ctx.Logger.Error(err.Error()) 48 | os.Exit(0) 49 | } 50 | 51 | cr.Storage.KafkaClient.Produce([]byte(add), b) 52 | 53 | return cmd, nil 54 | } 55 | 56 | // Read returns a Command from the 57 | // given ID. 58 | func (cr CommandRepoCtx) Read(id, user_id string) (entity.Command, error) { 59 | cmdFound := new(entity.Command) 60 | 61 | if err := cr.Storage.SQL.Client.First(&cmdFound, "id = ? AND user_id = ?", id, user_id).Error; err != nil { 62 | cr.Ctx.Logger.Error(err.Error()) 63 | return entity.Command{}, err 64 | } 65 | 66 | return *cmdFound, nil 67 | } 68 | 69 | // Update - update a command with the given fields 70 | // with the new values. 71 | func (cr CommandRepoCtx) Update(id string, cmd entity.Command) (entity.Command, error) { 72 | cmdNew := new(entity.Command) 73 | 74 | if err := cr.Storage.SQL.Client.First(&cmdNew, "id = ? AND user_id = ?", id, cmd.UserID.String()).Error; err != nil { 75 | cr.Ctx.Logger.Error(err.Error()) 76 | return entity.Command{}, err 77 | } 78 | 79 | cmdNew.UpdateFields(cmd) 80 | 81 | if err := cr.Storage.SQL.Client.Save(&cmdNew).Error; err != nil { 82 | cr.Ctx.Logger.Error(err.Error()) 83 | return entity.Command{}, err 84 | } 85 | jsonCommand := cmdNew.ToJSON() 86 | 87 | b, err := jsonCommand.ToJSONString() 88 | 89 | if err != nil { 90 | cr.Ctx.Logger.Error(err.Error()) 91 | os.Exit(0) 92 | } 93 | 94 | cr.Storage.KafkaClient.Produce([]byte(update), b) 95 | 96 | return *cmdNew, nil 97 | } 98 | 99 | // Delete removes a command from DB 100 | // with given ID 101 | func (cr CommandRepoCtx) Delete(id, user_id string) error { 102 | cmd := new(entity.Command) 103 | 104 | err := cr.Storage.SQL.Client.Where("id = ? AND user_id = ?", id, user_id).Delete(&cmd).Error 105 | 106 | if err != nil { 107 | cr.Ctx.Logger.Error(err.Error()) 108 | } 109 | 110 | jsonCommand := cmd.ToJSON() 111 | 112 | b, err := jsonCommand.ToJSONString() 113 | 114 | if err != nil { 115 | cr.Ctx.Logger.Error(err.Error()) 116 | os.Exit(0) 117 | } 118 | 119 | cr.Storage.KafkaClient.Produce([]byte(remove), b) 120 | 121 | return err 122 | } 123 | 124 | // NewCommandRepoCtx creates and return a 125 | // configured CommandRepoCtx. 126 | func NewCommandRepoCtx() CommandRepoCtx { 127 | return CommandRepoCtx{ 128 | Storage: config.Storage, 129 | Ctx: config.Ctx, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /services/api/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | "github.com/rafaelbreno/go-bot/api/config" 8 | "github.com/rafaelbreno/go-bot/api/handler" 9 | "github.com/rafaelbreno/go-bot/api/internal" 10 | "github.com/rafaelbreno/go-bot/api/middlewares" 11 | ) 12 | 13 | // Server manages HTTP 14 | type Server struct { 15 | HTTP *fiber.App 16 | Ctx *internal.Context 17 | } 18 | 19 | // Listen starts app 20 | func NewServer() *Server { 21 | srv := &Server{ 22 | HTTP: fiber.New(fiber.Config{ 23 | CaseSensitive: true, 24 | StrictRouting: false, 25 | Concurrency: 256 * 1024, 26 | WriteTimeout: time.Duration(45 * time.Second), 27 | }), 28 | Ctx: config.Ctx, 29 | } 30 | 31 | srv.routes() 32 | 33 | return srv 34 | } 35 | 36 | // ListenAndServe starts the web server. 37 | func (s *Server) ListenAndServe() { 38 | if err := s.HTTP.Listen(s.Ctx.Env["API_PORT"]); err != nil { 39 | s.Ctx.Logger.Error(err.Error()) 40 | } 41 | 42 | } 43 | 44 | // Close gracefully terminate the app. 45 | func (s *Server) Close() { 46 | s.Ctx.Logger.Info("Gracefully terminating API...") 47 | if err := s.HTTP.Shutdown(); err != nil { 48 | s.Ctx.Logger.Error(err.Error()) 49 | } 50 | } 51 | 52 | func (s *Server) routes() { 53 | s.commandRoutes() 54 | 55 | s.HTTP.Get("/ping", func(c *fiber.Ctx) error { 56 | s.Ctx.Logger.Info("GET /test") 57 | return c.JSON(map[string]string{ 58 | "message": "PONG", 59 | }) 60 | }) 61 | 62 | a := s.HTTP.Group("/test", middlewares.CheckAuth) 63 | 64 | a.Get("/ping", func(c *fiber.Ctx) error { 65 | s.Ctx.Logger.Info("GET /test") 66 | s.Ctx.Logger.Info(c.UserContext().Value("user_id").(string)) 67 | return c.JSON(map[string]string{ 68 | "message": "PONG", 69 | }) 70 | }) 71 | } 72 | 73 | func (s *Server) authRoutes() { 74 | authGroup := s.HTTP.Group("/auth") 75 | 76 | ah := handler.NewUserHandler(s.Ctx) 77 | 78 | authGroup.Post("/create", ah.Create) 79 | authGroup.Post("/login", ah.Login) 80 | authGroup.Post("/check", ah.Check) 81 | } 82 | 83 | func (s *Server) commandRoutes() { 84 | commandGroup := s.HTTP.Group("/command", middlewares.CheckAuth) 85 | 86 | ch := handler.NewCommandHandler() 87 | 88 | commandGroup.Post("/create", ch.Create) 89 | commandGroup.Get("/:id", ch.Read) 90 | commandGroup.Put("/:id", ch.Update) 91 | commandGroup.Patch("/:id", ch.Update) 92 | commandGroup.Delete("/:id", ch.Delete) 93 | } 94 | -------------------------------------------------------------------------------- /services/api/storage/kafka.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/rafaelbreno/go-bot/api/internal" 10 | "gopkg.in/confluentinc/confluent-kafka-go.v1/kafka" 11 | ) 12 | 13 | // KafkaClient manages kafka connection 14 | // and actions. 15 | type KafkaClient struct { 16 | Ctx *internal.Context 17 | P *kafka.Producer 18 | topic *string 19 | } 20 | 21 | func newKafkaClient(ctx *internal.Context) *KafkaClient { 22 | client := &KafkaClient{ 23 | Ctx: ctx, 24 | } 25 | 26 | client. 27 | createTopic(). 28 | setProducer() 29 | 30 | return client 31 | } 32 | 33 | func (k *KafkaClient) createTopic() *KafkaClient { 34 | t := new(string) 35 | *t = k.Ctx.Env["KAFKA_TOPIC"] 36 | k.topic = t 37 | 38 | kafkaAdmin, err := kafka.NewAdminClient(&kafka.ConfigMap{ 39 | "bootstrap.servers": fmt.Sprintf("%s:%s", k.Ctx.Env["KAFKA_URL"], k.Ctx.Env["KAFKA_PORT"]), 40 | }) 41 | 42 | defer kafkaAdmin.Close() 43 | 44 | if err != nil { 45 | k.Ctx.Logger.Error(err.Error()) 46 | return k 47 | } 48 | 49 | ctx, cancel := context.WithCancel(context.Background()) 50 | 51 | defer cancel() 52 | 53 | results, err := kafkaAdmin.CreateTopics( 54 | ctx, 55 | []kafka.TopicSpecification{{ 56 | Topic: k.Ctx.Env["KAFKA_TOPIC"], 57 | NumPartitions: 2, 58 | ReplicationFactor: 1, 59 | }}, 60 | kafka.SetAdminOperationTimeout(60*time.Second), 61 | ) 62 | 63 | if err != nil { 64 | k.Ctx.Logger.Error(err.Error()) 65 | return k 66 | } 67 | 68 | for _, res := range results { 69 | k.Ctx.Logger.Info(res.String()) 70 | } 71 | 72 | return k 73 | } 74 | 75 | func (k *KafkaClient) setProducer() *KafkaClient { 76 | p, err := kafka.NewProducer(&kafka.ConfigMap{ 77 | "bootstrap.servers": fmt.Sprintf("%s:%s", k.Ctx.Env["KAFKA_URL"], k.Ctx.Env["KAFKA_PORT"]), 78 | "group.id": 0, 79 | "enable.partition.eof": true, 80 | "group.instance.id": "1", 81 | "log_level": 2, 82 | }) 83 | 84 | k.Ctx.Logger.Info("Connecting into Kafka...") 85 | 86 | if err != nil { 87 | k.Ctx.Logger.Error(err.Error()) 88 | } 89 | 90 | if err := p.GetFatalError(); err != nil { 91 | k.Ctx.Logger.Error(err.Error()) 92 | os.Exit(0) 93 | } 94 | 95 | k.P = p 96 | 97 | return k 98 | } 99 | 100 | func (k *KafkaClient) Produce(key, value []byte) { 101 | deliveryChan := make(chan kafka.Event, 3) 102 | 103 | if err := k.P.Produce(&kafka.Message{ 104 | TopicPartition: kafka.TopicPartition{ 105 | Topic: k.topic, 106 | Partition: kafka.PartitionAny, 107 | }, 108 | Value: value, 109 | Key: key, 110 | }, deliveryChan); err != nil { 111 | if err != nil { 112 | k.Ctx.Logger.Error(err.Error()) 113 | os.Exit(0) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /services/api/storage/postgres.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/rafaelbreno/go-bot/api/internal" 8 | "gorm.io/driver/postgres" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // DB handler DB connection 13 | type DB struct { 14 | Client *gorm.DB 15 | Ctx *internal.Context 16 | } 17 | 18 | func newDB(ctx *internal.Context) *DB { 19 | var pgsqlURL string 20 | url := `host=%s port=%s user=%s password=%s dbname=%s sslmode=disable` 21 | 22 | pgsqlURL = fmt.Sprintf( 23 | url, 24 | ctx.Env["PGSQL_HOST"], 25 | ctx.Env["PGSQL_PORT"], 26 | ctx.Env["PGSQL_USER"], 27 | ctx.Env["PGSQL_PASSWORD"], 28 | ctx.Env["PGSQL_DBNAME"], 29 | ) 30 | 31 | db, err := gorm.Open(postgres.Open(pgsqlURL), &gorm.Config{}) 32 | 33 | if err != nil { 34 | ctx.Logger.Error(err.Error()) 35 | os.Exit(0) 36 | } 37 | 38 | return &DB{ 39 | Ctx: ctx, 40 | Client: db, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services/api/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/rafaelbreno/go-bot/api/internal" 5 | ) 6 | 7 | // Storage manages connections between 8 | // this API and any resource of storage 9 | // e.g(Postgres, S3, etc.). 10 | type Storage struct { 11 | SQL *DB 12 | KafkaClient *KafkaClient 13 | Ctx *internal.Context 14 | } 15 | 16 | // NewStorage return a storage manager 17 | // for database and in-memory 18 | func NewStorage(ctx *internal.Context) *Storage { 19 | return &Storage{ 20 | Ctx: ctx, 21 | SQL: newDB(ctx), 22 | KafkaClient: newKafkaClient(ctx), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/auth/.env.example: -------------------------------------------------------------------------------- 1 | AUTH_PORT=5000 2 | 3 | PGSQL_DATA=db 4 | PGSQL_HOST=localhost 5 | PGSQL_PORT=5432 6 | PGSQL_USER=root 7 | PGSQL_PASSWORD=root 8 | PGSQL_DBNAME=project 9 | PGSQL_NAME=bot-postgres 10 | 11 | REDIS_HOST=localhost 12 | REDIS_PORT=6379 13 | REDIS_NAME=bot-redis 14 | REDIS_DB=0 15 | REDIS_PASSWORD= 16 | -------------------------------------------------------------------------------- /services/auth/.gitignore: -------------------------------------------------------------------------------- 1 | proto/*.go 2 | -------------------------------------------------------------------------------- /services/auth/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/rafaelbreno/go-bot/auth/internal" 7 | "github.com/rafaelbreno/go-bot/auth/proto" 8 | "github.com/rafaelbreno/go-bot/auth/server" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | func main() { 13 | lis, err := net.Listen("tcp", "localhost:5004") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | var opts []grpc.ServerOption 19 | 20 | grpcServer := grpc.NewServer(opts...) 21 | 22 | if err != nil { 23 | panic(err) 24 | } 25 | proto.RegisterAuthServer(grpcServer, server.NewServer(internal.NewCommon())) 26 | 27 | grpcServer.Serve(lis) 28 | } 29 | -------------------------------------------------------------------------------- /services/auth/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rafaelbreno/go-bot/auth 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-pg/pg/v10 v10.10.6 7 | github.com/go-redis/redis/v8 v8.11.4 8 | github.com/google/uuid v1.3.0 9 | github.com/joho/godotenv v1.4.0 10 | go.uber.org/zap v1.19.1 11 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 12 | google.golang.org/grpc v1.32.0 13 | google.golang.org/protobuf v1.26.0 14 | ) 15 | 16 | require ( 17 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 18 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 19 | github.com/go-pg/zerochecker v0.2.0 // indirect 20 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 21 | github.com/golang/protobuf v1.5.2 // indirect 22 | github.com/jinzhu/inflection v1.0.0 // indirect 23 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect 24 | github.com/vmihailenco/bufpool v0.1.11 // indirect 25 | github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect 26 | github.com/vmihailenco/tagparser v0.1.2 // indirect 27 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 28 | go.uber.org/atomic v1.9.0 // indirect 29 | go.uber.org/multierr v1.7.0 // indirect 30 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect 31 | golang.org/x/sys v0.0.0-20210923061019-b8560ed6a9b7 // indirect 32 | golang.org/x/text v0.3.6 // indirect 33 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect 34 | mellium.im/sasl v0.2.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /services/auth/internal/common.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/joho/godotenv" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type Common struct { 12 | Env Env 13 | Logger *zap.Logger 14 | } 15 | 16 | type Env struct { 17 | PgHost string 18 | PgPort string 19 | PgUser string 20 | PgPassword string 21 | PgDBName string 22 | PgName string 23 | 24 | RedisHost string 25 | RedisPort string 26 | RedisName string 27 | RedisDB int 28 | RedisPassword string 29 | } 30 | 31 | func NewCommon() *Common { 32 | l, _ := zap.NewProduction() 33 | 34 | if os.Getenv("IS_PROD") != "true" { 35 | l.Info("Loading .env file") 36 | if err := godotenv.Load(); err != nil { 37 | l.Error(err.Error()) 38 | } 39 | } 40 | 41 | redisDB, err := strconv.Atoi(os.Getenv("REDIS_DB")) 42 | 43 | if err != nil { 44 | l.Error(err.Error()) 45 | return &Common{} 46 | } 47 | 48 | c := Common{ 49 | Logger: l, 50 | Env: Env{ 51 | PgHost: os.Getenv("PGSQL_HOST"), 52 | PgPassword: os.Getenv("PGSQL_PASSWORD"), 53 | PgUser: os.Getenv("PGSQL_USER"), 54 | PgPort: os.Getenv("PGSQL_PORT"), 55 | PgDBName: os.Getenv("PGSQL_DBNAME"), 56 | PgName: os.Getenv("PGSQL_NAME"), 57 | RedisHost: os.Getenv("REDIS_HOST"), 58 | RedisPort: os.Getenv("REDIS_PORT"), 59 | RedisName: os.Getenv("REDIS_NAME"), 60 | RedisDB: redisDB, 61 | RedisPassword: os.Getenv("REDIS_PASSWORD"), 62 | }, 63 | } 64 | 65 | return &c 66 | } 67 | -------------------------------------------------------------------------------- /services/auth/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt" 8 | "github.com/rafaelbreno/go-bot/auth/storage" 9 | ) 10 | 11 | type JWT struct { 12 | Storage *storage.Storage 13 | } 14 | 15 | func NewJWT(sto *storage.Storage) *JWT { 16 | return &JWT{ 17 | Storage: sto, 18 | } 19 | } 20 | 21 | var ( 22 | MY_SIGN = []byte(`some_sign`) 23 | ) 24 | 25 | type MyClaim struct { 26 | UserID string `json:"user_id"` 27 | jwt.StandardClaims 28 | } 29 | 30 | func (j *JWT) NewToken(id string) (string, error) { 31 | myClaim := MyClaim{ 32 | id, 33 | jwt.StandardClaims{ 34 | ExpiresAt: time.Now().Add(time.Hour * 8).Unix(), 35 | Issuer: "test", 36 | }, 37 | } 38 | 39 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaim) 40 | 41 | return token.SignedString(MY_SIGN) 42 | } 43 | 44 | func (j *JWT) CheckToken(tokenStr string) error { 45 | token, err := jwt.Parse(tokenStr, func(*jwt.Token) (interface{}, error) { 46 | return MY_SIGN, nil 47 | }) 48 | 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if token.Valid { 54 | return nil 55 | } else if ve, ok := err.(*jwt.ValidationError); ok { 56 | if ve.Errors&jwt.ValidationErrorMalformed != 0 { 57 | return fmt.Errorf("invalid token") 58 | } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { 59 | // Token is either expired or not active yet 60 | return fmt.Errorf("expired token") 61 | } else { 62 | return err 63 | } 64 | } 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /services/auth/proto/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | option go_package = "/proto"; 5 | 6 | service Auth { 7 | rpc Create(CreateRequest) returns (CreateResponse) {} 8 | rpc Login(LoginRequest) returns (LoginResponse) {} 9 | rpc Check(CheckRequest) returns (CheckResponse) {} 10 | } 11 | 12 | message CreateRequest { 13 | string username = 1; 14 | string password = 2; 15 | string password_confirmation = 3; 16 | } 17 | 18 | message CreateResponse { 19 | string token = 1; 20 | string error = 2; 21 | } 22 | 23 | message LoginRequest { 24 | string username = 1; 25 | string password = 2; 26 | } 27 | 28 | message LoginResponse { 29 | string token = 1; 30 | string error = 2; 31 | } 32 | 33 | message CheckRequest { 34 | string token = 1; 35 | } 36 | 37 | message CheckResponse { 38 | string error = 1; 39 | } 40 | -------------------------------------------------------------------------------- /services/auth/repository/auth.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-pg/pg/v10" 7 | "github.com/rafaelbreno/go-bot/auth/internal" 8 | "github.com/rafaelbreno/go-bot/auth/jwt" 9 | "github.com/rafaelbreno/go-bot/auth/proto" 10 | "github.com/rafaelbreno/go-bot/auth/storage" 11 | "github.com/rafaelbreno/go-bot/auth/user" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | type AuthRepo interface { 16 | Create(req *proto.CreateRequest) *proto.CreateResponse 17 | Login(req *proto.LoginRequest) *proto.LoginResponse 18 | Check(req *proto.CheckRequest) *proto.CheckResponse 19 | } 20 | 21 | type AuthRepoCtx struct { 22 | Common *internal.Common 23 | JWT *jwt.JWT 24 | Storage *storage.Storage 25 | } 26 | 27 | func NewAuthRepo(c *internal.Common, sto *storage.Storage) *AuthRepoCtx { 28 | a := AuthRepoCtx{ 29 | Common: c, 30 | Storage: sto, 31 | JWT: jwt.NewJWT(sto), 32 | } 33 | return &a 34 | } 35 | 36 | const ( 37 | fieldsNotMatch = `'%s' and '%s' don't match` 38 | emptyField = `field '%s' can not be empty` 39 | wrongPassord = `wrong password` 40 | ) 41 | 42 | func (a *AuthRepoCtx) Create(req *proto.CreateRequest) *proto.CreateResponse { 43 | if req.Username == "" { 44 | return &proto.CreateResponse{ 45 | Error: fmt.Sprintf(emptyField, "username"), 46 | } 47 | } 48 | if req.Password == "" { 49 | return &proto.CreateResponse{ 50 | Error: fmt.Sprintf(emptyField, "password"), 51 | } 52 | } 53 | 54 | if req.Password != req.PasswordConfirmation { 55 | errMsg := fmt.Sprintf(fieldsNotMatch, "password", "password_confirmation") 56 | return &proto.CreateResponse{ 57 | Token: "", 58 | Error: errMsg, 59 | } 60 | } 61 | 62 | encPW, err := a.EncPassword(req.Password) 63 | 64 | if err != nil { 65 | return &proto.CreateResponse{ 66 | Error: err.Error(), 67 | } 68 | } 69 | 70 | u := user.User{ 71 | Username: req.Username, 72 | Password: string(encPW), 73 | } 74 | 75 | if _, err := a.Storage.Pg.Conn.Model(&u).Insert(); err != nil { 76 | return &proto.CreateResponse{ 77 | Error: err.Error(), 78 | } 79 | } 80 | token, err := a.JWT.NewToken(u.Id.String()) 81 | 82 | if err != nil { 83 | return &proto.CreateResponse{ 84 | Error: err.Error(), 85 | } 86 | } 87 | return &proto.CreateResponse{ 88 | Token: token, 89 | } 90 | } 91 | 92 | func (a *AuthRepoCtx) Login(req *proto.LoginRequest) *proto.LoginResponse { 93 | if req.Username == "" { 94 | return &proto.LoginResponse{ 95 | Error: fmt.Sprintf(emptyField, "username"), 96 | } 97 | } 98 | 99 | if req.Password == "" { 100 | return &proto.LoginResponse{ 101 | Error: fmt.Sprintf(emptyField, "password"), 102 | } 103 | } 104 | 105 | u := new(user.User) 106 | 107 | if err := a.Storage.Pg.Conn. 108 | Model(u). 109 | Where("? = ?", pg.Ident("username"), req.Username). 110 | Limit(1).Select(); err != nil { 111 | return &proto.LoginResponse{ 112 | Error: err.Error(), 113 | } 114 | } 115 | 116 | if !a.CheckPassword(u.Password, req.Password) { 117 | return &proto.LoginResponse{ 118 | Error: wrongPassord, 119 | } 120 | } 121 | 122 | token, err := a.JWT.NewToken(u.Id.String()) 123 | 124 | if err != nil { 125 | return &proto.LoginResponse{ 126 | Error: err.Error(), 127 | } 128 | } 129 | return &proto.LoginResponse{ 130 | Token: token, 131 | } 132 | } 133 | 134 | func (a *AuthRepoCtx) Check(req *proto.CheckRequest) *proto.CheckResponse { 135 | var errMsg string 136 | if err := a.JWT.CheckToken(req.Token); err != nil { 137 | errMsg = err.Error() 138 | } 139 | 140 | return &proto.CheckResponse{ 141 | Error: errMsg, 142 | } 143 | } 144 | 145 | func (a *AuthRepoCtx) EncPassword(pw string) (string, error) { 146 | encPW, err := bcrypt.GenerateFromPassword([]byte(pw), 14) 147 | return string(encPW), err 148 | } 149 | 150 | func (a *AuthRepoCtx) CheckPassword(encPW, pw string) bool { 151 | err := bcrypt.CompareHashAndPassword([]byte(encPW), []byte(pw)) 152 | return err == nil 153 | } 154 | -------------------------------------------------------------------------------- /services/auth/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rafaelbreno/go-bot/auth/internal" 7 | "github.com/rafaelbreno/go-bot/auth/proto" 8 | "github.com/rafaelbreno/go-bot/auth/repository" 9 | "github.com/rafaelbreno/go-bot/auth/storage" 10 | ) 11 | 12 | type Server struct { 13 | proto.UnimplementedAuthServer 14 | Common *internal.Common 15 | Repo *repository.AuthRepoCtx 16 | } 17 | 18 | func (s *Server) Create(ctx context.Context, req *proto.CreateRequest) (*proto.CreateResponse, error) { 19 | return s.Repo.Create(req), nil 20 | } 21 | 22 | func (s *Server) Login(ctx context.Context, req *proto.LoginRequest) (*proto.LoginResponse, error) { 23 | return s.Repo.Login(req), nil 24 | } 25 | 26 | func (s *Server) Check(ctx context.Context, req *proto.CheckRequest) (*proto.CheckResponse, error) { 27 | return s.Repo.Check(req), nil 28 | } 29 | 30 | func NewServer(common *internal.Common) *Server { 31 | return &Server{ 32 | Common: common, 33 | Repo: repository.NewAuthRepo(common, storage.NewStorage(common)), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /services/auth/storage/pg.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | pg "github.com/go-pg/pg/v10" 5 | "github.com/go-pg/pg/v10/orm" 6 | "github.com/rafaelbreno/go-bot/auth/internal" 7 | "github.com/rafaelbreno/go-bot/auth/user" 8 | ) 9 | 10 | type Postgres struct { 11 | common *internal.Common 12 | Conn *pg.DB 13 | } 14 | 15 | func NewPostgres(c *internal.Common) *Postgres { 16 | p := Postgres{ 17 | common: c, 18 | } 19 | 20 | p. 21 | setConnection(). 22 | migration() 23 | 24 | return &p 25 | } 26 | 27 | func (p *Postgres) setConnection() *Postgres { 28 | p.Conn = pg.Connect(&pg.Options{ 29 | Addr: p.getAddress(), 30 | User: p.common.Env.PgUser, 31 | Password: p.common.Env.PgPassword, 32 | Database: p.common.Env.PgDBName, 33 | }) 34 | return p 35 | } 36 | 37 | func (p *Postgres) getAddress() string { 38 | if p.common.Env.PgPort == "" { 39 | return p.common.Env.PgHost 40 | } 41 | return string(p.common.Env.PgHost + ":" + p.common.Env.PgPort) 42 | } 43 | 44 | func (p *Postgres) migration() { 45 | models := []interface{}{ 46 | (*user.User)(nil), 47 | } 48 | 49 | for _, m := range models { 50 | if err := p.Conn.Model(m).CreateTable(&orm.CreateTableOptions{ 51 | Temp: false, 52 | }); err != nil { 53 | p.common.Logger.Error(err.Error()) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /services/auth/storage/redis.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | "github.com/rafaelbreno/go-bot/auth/internal" 6 | ) 7 | 8 | type Redis struct { 9 | common *internal.Common 10 | Conn *redis.Client 11 | } 12 | 13 | func NewRedis(c *internal.Common) *Redis { 14 | r := Redis{ 15 | common: c, 16 | } 17 | 18 | r.setConnection() 19 | 20 | return &r 21 | } 22 | 23 | func (r *Redis) setConnection() { 24 | r.Conn = redis.NewClient(&redis.Options{ 25 | Addr: r.getAddress(), 26 | Password: r.common.Env.RedisPassword, 27 | DB: r.common.Env.RedisDB, 28 | }) 29 | } 30 | 31 | func (r *Redis) getAddress() string { 32 | if r.common.Env.RedisPort == "" { 33 | return r.common.Env.RedisHost 34 | } 35 | 36 | return string(r.common.Env.RedisHost + ":" + r.common.Env.RedisPort) 37 | } 38 | -------------------------------------------------------------------------------- /services/auth/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/rafaelbreno/go-bot/auth/internal" 5 | ) 6 | 7 | type Storage struct { 8 | Redis *Redis 9 | Pg *Postgres 10 | } 11 | 12 | func NewStorage(c *internal.Common) *Storage { 13 | s := Storage{ 14 | Redis: NewRedis(c), 15 | Pg: NewPostgres(c), 16 | } 17 | 18 | return &s 19 | } 20 | -------------------------------------------------------------------------------- /services/auth/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "github.com/google/uuid" 4 | 5 | type User struct { 6 | Id uuid.UUID `pg:"type:uuid,pk,default:gen_random_uuid()"` 7 | Username string `pg:"username,unique"` 8 | Password string 9 | } 10 | -------------------------------------------------------------------------------- /services/bot/bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rafaelbreno/go-bot/command" 7 | "github.com/rafaelbreno/go-bot/conn" 8 | "github.com/rafaelbreno/go-bot/internal" 9 | "github.com/rafaelbreno/go-bot/utils" 10 | ) 11 | 12 | // Bootstrap manages all actions 13 | // related to the bot itself 14 | type Bootstrap struct { 15 | Ctx *internal.Context 16 | IRC *conn.IRC 17 | Command *command.CommandCtx 18 | MsgChan chan string 19 | } 20 | 21 | // Start ignites the bot 22 | func Start(ctx *internal.Context, irc *conn.IRC) { 23 | ch := make(chan string, 1) 24 | 25 | b := &Bootstrap{ 26 | Ctx: ctx, 27 | IRC: irc, 28 | Command: &command.CommandCtx{ 29 | Ctx: ctx, 30 | H: command.NewCMDHelper(ctx), 31 | }, 32 | MsgChan: ch, 33 | } 34 | 35 | b.Ctx.Logger.Info("Start bot") 36 | 37 | go b.IRC.Listen(ch) 38 | go b.receiveMsg() 39 | } 40 | 41 | func (b *Bootstrap) receiveMsg() { 42 | b.Ctx.Logger.Info("Start parser") 43 | p := NewParser(b.Ctx) 44 | for { 45 | select { 46 | case msgStr := <-b.MsgChan: 47 | msg := p.ParseMsg(msgStr) 48 | msg.Ctx = b.Ctx 49 | b.do(msg) 50 | } 51 | } 52 | } 53 | 54 | func (b *Bootstrap) do(msg *Message) { 55 | switch msg.Type { 56 | case Nil: 57 | break 58 | case Ping: 59 | utils.Write(b.Ctx, b.IRC.Conn, "PONG") 60 | break 61 | case Command: 62 | msgStr := fmt.Sprintf("PRIVMSG #%s :%s", b.Ctx.ChannelName, msg.getString(b)) 63 | b.Ctx.Logger.Info(msgStr) 64 | utils.Write(b.Ctx, b.IRC.Conn, msgStr) 65 | break 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /services/bot/bot/message.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import "github.com/rafaelbreno/go-bot/internal" 4 | 5 | // MsgType defines which message 6 | // type was sent 7 | type MsgType int 8 | 9 | const ( 10 | // Nil takes no action 11 | Nil MsgType = iota 12 | // Twitch 's communications 13 | Twitch 14 | // Ping to shakehands with Twitch 15 | Ping 16 | // Command is prefixed by exclamation mark ! 17 | Command 18 | ) 19 | 20 | // Message stores all information related 21 | // to a sent message 22 | type Message struct { 23 | Ctx *internal.Context 24 | Type MsgType 25 | SentBy string 26 | SentMessage string 27 | } 28 | 29 | func (m *Message) getString(b *Bootstrap) string { 30 | return b.Command.GetAnswer(m.SentBy, m.SentMessage) 31 | } 32 | -------------------------------------------------------------------------------- /services/bot/bot/message_parser.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/rafaelbreno/go-bot/internal" 9 | ) 10 | 11 | type Parser struct { 12 | Ctx *internal.Context 13 | // UserRegex retrieves username 14 | UserRegex *regexp.Regexp 15 | 16 | // MessageRegex retrieves user's message 17 | MessageRegex *regexp.Regexp 18 | 19 | channelPrefix string 20 | } 21 | 22 | func NewParser(ctx *internal.Context) *Parser { 23 | msgRegexStr := fmt.Sprintf(`(#%s :).{1,}$`, ctx.ChannelName) 24 | ctx.Logger.Info("Initialzied parser") 25 | return &Parser{ 26 | Ctx: ctx, 27 | UserRegex: regexp.MustCompile(`^(:)[a-zA-Z0-9_]{1,}(!)`), 28 | MessageRegex: regexp.MustCompile(msgRegexStr), 29 | channelPrefix: fmt.Sprintf("#%s :", ctx.ChannelName), 30 | } 31 | } 32 | 33 | // ParseMsg a string into a 34 | // Message struct 35 | func (p *Parser) ParseMsg(msgStr string) *Message { 36 | fmt.Println(msgStr) 37 | 38 | if strings.HasPrefix(msgStr, "PING") { 39 | p.Ctx.Logger.Info("Received ping") 40 | return &Message{ 41 | Type: Ping, 42 | } 43 | } 44 | if strings.HasPrefix(msgStr, ":tmi.twitch.tv") { 45 | return &Message{ 46 | Type: Nil, 47 | } 48 | } 49 | 50 | sentMessageRaw := p.MessageRegex.FindString(msgStr) 51 | sentByRaw := p.UserRegex.Find([]byte(msgStr)) 52 | lenSentBy := len(sentByRaw) 53 | 54 | if lenSentBy == 0 { 55 | p.Ctx.Logger.Info("lenSentBy = 0") 56 | return &Message{ 57 | Type: Nil, 58 | } 59 | } 60 | 61 | sentMessage := strings.TrimPrefix(sentMessageRaw, p.channelPrefix) 62 | sentBy := string(sentByRaw[1 : lenSentBy-1]) 63 | 64 | if !strings.HasPrefix(sentMessage, "!") { 65 | return &Message{ 66 | Type: Nil, 67 | } 68 | } 69 | 70 | return &Message{ 71 | Type: Command, 72 | SentMessage: sentMessage, 73 | SentBy: sentBy, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /services/bot/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "strings" 7 | "syscall" 8 | 9 | "github.com/joho/godotenv" 10 | "github.com/rafaelbreno/go-bot/bot" 11 | "github.com/rafaelbreno/go-bot/conn" 12 | "github.com/rafaelbreno/go-bot/internal" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | var ( 17 | connection []*conn.IRC 18 | ctxs map[string]*internal.Context 19 | logger *zap.Logger 20 | ) 21 | 22 | func newLogger() { 23 | logger, _ = zap.NewProduction() 24 | } 25 | 26 | func setConnections() { 27 | for _, ctx := range ctxs { 28 | c, err := conn.NewIRC(ctx) 29 | if err != nil { 30 | logger.Error(err.Error()) 31 | os.Exit(0) 32 | } 33 | 34 | connection = append(connection, c) 35 | } 36 | } 37 | 38 | func init() { 39 | newLogger() 40 | 41 | // Loading .env file 42 | if err := godotenv.Load(); err != nil { 43 | logger.Error(err.Error()) 44 | } 45 | 46 | ctxs = internal.WriteContexts(logger, 47 | os.Getenv("BOT_OAUTH_TOKEN"), 48 | os.Getenv("BOT_USERNAME"), 49 | strings.Split(os.Getenv("CHANNEL_NAME"), ",")) 50 | 51 | setConnections() 52 | } 53 | 54 | func main() { 55 | stop := make(chan os.Signal, 1) 56 | signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 57 | 58 | for _, c := range connection { 59 | go bot.Start(c.Ctx, c) 60 | 61 | defer c.Close() 62 | } 63 | 64 | <-stop 65 | 66 | logger.Info("Gracefully terminating bot") 67 | } 68 | -------------------------------------------------------------------------------- /services/bot/command/blacklist.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | var ( 4 | modBlacklist = []string{ 5 | "nightbot", "streamelements", "anotherttvviewer", 6 | "creatisbot", "streamholics", "Federicofeliny", 7 | "orsonw", "commanderroot", 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /services/bot/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "math/rand" 5 | "regexp" 6 | "time" 7 | 8 | "github.com/rafaelbreno/go-bot/internal" 9 | ) 10 | 11 | // CommandCtx stores data to prepare 12 | // the messages to be sent 13 | type CommandCtx struct { 14 | Ctx *internal.Context 15 | H *CmdHelper 16 | } 17 | 18 | // Command store commands 19 | type Command struct { 20 | Key string 21 | Answer string 22 | Options []string 23 | Type int 24 | HasCooldown bool 25 | Cooldown time.Duration 26 | ExpireAt int64 27 | } 28 | 29 | type Action struct { 30 | SentBy string 31 | } 32 | 33 | var commands = map[string]*Command{ 34 | "!hello": { 35 | Key: "!hello", 36 | Type: Simple, 37 | Answer: "Hello, {user}!", 38 | HasCooldown: true, 39 | Cooldown: time.Duration(15 * time.Second), 40 | ExpireAt: 0, 41 | }, 42 | "!signo": { 43 | Key: "!signo", 44 | Type: Random, 45 | HasCooldown: false, 46 | Answer: "/me {user} decidiu trocar de signo, agora seu novo signo é: {answer}", 47 | Options: lstSigno, 48 | }, 49 | "!cupido": { 50 | Key: "!cupido", 51 | Type: Cupido, 52 | HasCooldown: false, 53 | Answer: "/me {user} sua alma gêmea é: @{user_list}", 54 | }, 55 | } 56 | 57 | var ( 58 | cmdRegex = regexp.MustCompile(`^(!)[a-zA-Z0-9]{1,}`) 59 | ) 60 | 61 | // GetAnswer receives a message to be sent 62 | func (c *CommandCtx) GetAnswer(sentBy, inMessage string) string { 63 | cmdKey := string(cmdRegex.Find([]byte(inMessage))) 64 | 65 | var cmd *Command 66 | var ok bool 67 | 68 | if cmd, ok = commands[cmdKey]; !ok { 69 | return "" 70 | } 71 | 72 | action := &Action{ 73 | SentBy: sentBy, 74 | } 75 | 76 | return cmd.prepare(action, c) 77 | } 78 | 79 | type keyMap map[string]string 80 | 81 | func (c *Command) prepare(act *Action, ctx *CommandCtx) string { 82 | timeNow := time.Now() 83 | timeNowUnix := timeNow.Unix() 84 | 85 | rand.Seed(timeNowUnix) 86 | 87 | if c.HasCooldown { 88 | if !(c.ExpireAt == 0 || c.ExpireAt <= timeNowUnix) { 89 | return "" 90 | } 91 | c.ExpireAt = timeNow.Add(c.Cooldown).Unix() 92 | } 93 | 94 | switch c.Type { 95 | case Simple: 96 | return replace(c.Answer, keyMap{ 97 | "{user}": act.SentBy, 98 | }) 99 | case Random: 100 | if act.SentBy == "rafiusky" { 101 | return replace(c.Answer, keyMap{ 102 | "{user}": act.SentBy, 103 | "{answer}": "O Glorioso", 104 | }) 105 | } 106 | if act.SentBy == "lajurubeba" { 107 | return replace(c.Answer, keyMap{ 108 | "{user}": act.SentBy, 109 | "{answer}": "espirro de loli", 110 | }) 111 | } 112 | return replace(c.Answer, keyMap{ 113 | "{user}": act.SentBy, 114 | "{answer}": random(c.Options), 115 | }) 116 | case Cupido: 117 | ans := "" 118 | 119 | ans = random(ctx.H.fetchUserList(), append(modBlacklist, "lajurubeba", "rafiusky", "rafiuskybot", act.SentBy)...) 120 | 121 | return replace(c.Answer, keyMap{ 122 | "{user}": act.SentBy, 123 | "{user_list}": ans, 124 | }) 125 | default: 126 | return "" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /services/bot/command/command_type.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | const ( 4 | Simple int = iota 5 | Random 6 | Cupido 7 | ) 8 | -------------------------------------------------------------------------------- /services/bot/command/consts.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | var ( 4 | lstSigno = []string{ 5 | "batata", "cadeira de massagem ", "brownie de feijão", 6 | "chuveiro frio", "carioca", "macarrao de arroz", "fofoqueiro", 7 | "crókxi", "espirro de loli", "galinha de galocha", 8 | "teta", "helicoptero de ataque", 9 | } 10 | 11 | cupidPair = map[string]string{ 12 | "rafiusky": "lajurubeba", 13 | "lajurubeba": "rafiusky", 14 | "carrinheiro": "Angelina", 15 | "johncharlesps": "00bex", 16 | "keshinho1": "iziph", 17 | "iziph": "keshinho1", 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /services/bot/command/helper.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/rafaelbreno/go-bot/internal" 12 | ) 13 | 14 | type TMIViewers struct { 15 | Chatters struct { 16 | Broadcaster []string `json:"broadcaster"` 17 | Vips []string `json:"vips"` 18 | Moderators []string `json:"moderators"` 19 | Viewers []string `json:"viewers"` 20 | } `json:"chatters"` 21 | } 22 | 23 | type CmdHelper struct { 24 | ctx *internal.Context 25 | userURL string 26 | } 27 | 28 | func NewCMDHelper(ctx *internal.Context) *CmdHelper { 29 | return &CmdHelper{ 30 | ctx: ctx, 31 | userURL: fmt.Sprintf("https://tmi.twitch.tv/group/user/%s/chatters", ctx.ChannelName), 32 | } 33 | } 34 | 35 | func (c *CmdHelper) fetchUserList() []string { 36 | v := TMIViewers{} 37 | resp, _ := http.Get(c.userURL) 38 | _ = json.NewDecoder(resp.Body).Decode(&v) 39 | 40 | var list []string 41 | list = append(list, v.Chatters.Broadcaster...) 42 | list = append(list, v.Chatters.Viewers...) 43 | list = append(list, v.Chatters.Vips...) 44 | list = append(list, v.Chatters.Moderators...) 45 | return list 46 | } 47 | 48 | func random(list []string, blackList ...string) string { 49 | rand.Seed(time.Now().Unix()) 50 | 51 | if len(blackList) == 0 { 52 | return list[rand.Intn(len(list))] 53 | } 54 | 55 | item := "" 56 | for { 57 | item = list[rand.Intn(len(list))] 58 | if !find(blackList, item) { 59 | break 60 | } 61 | } 62 | return item 63 | } 64 | 65 | func replace(str string, repMap keyMap) string { 66 | for key, val := range repMap { 67 | str = strings.ReplaceAll(str, key, val) 68 | } 69 | return str 70 | } 71 | 72 | func find(slice []string, val string) bool { 73 | for _, item := range slice { 74 | if item == val { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | -------------------------------------------------------------------------------- /services/bot/conn/irc.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "net/textproto" 8 | "os" 9 | "time" 10 | 11 | "github.com/rafaelbreno/go-bot/internal" 12 | ) 13 | 14 | const ( 15 | ircConnURL = `%s:%s` 16 | ) 17 | 18 | // IRC stores the information 19 | // and actions related to IRC 20 | // connection 21 | type IRC struct { 22 | Conn net.Conn 23 | Ctx *internal.Context 24 | TP *textproto.Reader 25 | Msg chan string 26 | } 27 | 28 | // NewIRC returns a IRC 29 | // struct pointer with 30 | // configured connection 31 | func NewIRC(ctx *internal.Context) (*IRC, error) { 32 | return &IRC{ 33 | Ctx: ctx, 34 | Msg: make(chan string, 1), 35 | }, nil 36 | } 37 | 38 | // Listen start listen IRC channel, 39 | // and if it disconnects, it will try 40 | // to reconnect 3 times 41 | func (i *IRC) Listen(ch chan string) { 42 | i.connect() 43 | 44 | go func() { 45 | for { 46 | msg, err := i.TP.ReadLine() 47 | if err != nil { 48 | close(i.Msg) 49 | i.Ctx.Logger.Error(err.Error()) 50 | i.connect() 51 | continue 52 | } 53 | ch <- msg 54 | } 55 | }() 56 | } 57 | 58 | func (i *IRC) connect() { 59 | connStr := fmt.Sprintf(ircConnURL, os.Getenv("IRC_URL"), os.Getenv("IRC_PORT")) 60 | 61 | var c net.Conn 62 | var err error 63 | 64 | connected := false 65 | 66 | for tries := 1; tries <= 3; tries++ { 67 | c, err = net.Dial("tcp", connStr) 68 | if err == nil { 69 | i.Conn = c 70 | connected = true 71 | break 72 | } 73 | errMsg := fmt.Sprintf("Error %s. Try number %d!", err.Error(), tries) 74 | i.Ctx.Logger.Error(errMsg) 75 | time.Sleep(2 * time.Second) 76 | } 77 | 78 | if !connected { 79 | i.Ctx.Logger.Error("Unable to connect to IRC") 80 | os.Exit(0) 81 | } 82 | 83 | pass := fmt.Sprintf("PASS %s\r\n", i.Ctx.OAuthToken) 84 | if _, err := fmt.Fprint(i.Conn, pass); err != nil { 85 | i.Ctx.Logger.Error(err.Error()) 86 | os.Exit(0) 87 | } 88 | nick := fmt.Sprintf("NICK %s\r\n", i.Ctx.BotName) 89 | if _, err := fmt.Fprint(i.Conn, nick); err != nil { 90 | i.Ctx.Logger.Error(err.Error()) 91 | os.Exit(0) 92 | } 93 | i.Ctx.Logger.Info(nick) 94 | join := fmt.Sprintf("JOIN #%s\r\n", i.Ctx.ChannelName) 95 | if _, err := fmt.Fprint(i.Conn, join); err != nil { 96 | i.Ctx.Logger.Error(err.Error()) 97 | os.Exit(0) 98 | } 99 | i.Ctx.Logger.Info(join) 100 | 101 | i.TP = textproto.NewReader(bufio.NewReader(i.Conn)) 102 | i.Msg = make(chan string, 1) 103 | } 104 | 105 | // Close ends IRC connection 106 | func (i *IRC) Close() { 107 | i.Ctx.Logger.Info("Closing IRC connection") 108 | if err := i.Conn.Close(); err != nil { 109 | i.Ctx.Logger.Error(err.Error()) 110 | os.Exit(0) 111 | } 112 | i.Ctx.Logger.Info("IRC connection closed") 113 | } 114 | -------------------------------------------------------------------------------- /services/bot/conn/irc_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/rafaelbreno/go-bot/internal" 8 | "github.com/rafaelbreno/go-bot/test" 9 | ) 10 | 11 | func TestIRC(t *testing.T) { 12 | tts := []test.TestCases{} 13 | 14 | { 15 | os.Setenv("IRC_URL", "irc://irc.chat.twitch.tv") 16 | os.Setenv("IRC_PORT", "6667") 17 | conn, err := NewIRC(&internal.Context{}) 18 | //netConn, _ := net.Dial("tcp", fmt.Sprintf(ircConnURL, os.Getenv("IRC_URL"), os.Getenv("IRC_PORT"))) 19 | connWant := &IRC{ 20 | Ctx: &internal.Context{}, 21 | Msg: make(chan string, 1), 22 | } 23 | tts = append(tts, test.TestCases{ 24 | Name: "NewIRC OK - IRC", 25 | Got: conn.Ctx, 26 | Want: connWant.Ctx, 27 | TestType: test.Equal, 28 | }) 29 | tts = append(tts, test.TestCases{ 30 | Name: "NewIRC OK - Error", 31 | Got: err, 32 | TestType: test.Nil, 33 | }) 34 | } 35 | 36 | test.RunTests(t, tts) 37 | } 38 | -------------------------------------------------------------------------------- /services/bot/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rafaelbreno/go-bot 2 | 3 | // +heroku goVersion go1.16 4 | go 1.16 5 | 6 | require ( 7 | github.com/joho/godotenv v1.3.0 8 | github.com/stretchr/testify v1.7.0 9 | go.uber.org/atomic v1.9.0 // indirect 10 | go.uber.org/multierr v1.7.0 // indirect 11 | go.uber.org/zap v1.19.0 12 | ) 13 | -------------------------------------------------------------------------------- /services/bot/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 7 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 8 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 14 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 19 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 20 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 23 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 24 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 25 | go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= 26 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 27 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 28 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 29 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 30 | go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= 31 | go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 32 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 33 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 34 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 35 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 37 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 41 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= 42 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 43 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 46 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 49 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 52 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /services/bot/internal/context.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | // Context stores informations related 8 | // to the connection 9 | type Context struct { 10 | // Logger access a logging package 11 | // to log all action inside the bot 12 | Logger *zap.Logger 13 | ChannelName string 14 | OAuthToken string 15 | BotName string 16 | } 17 | 18 | // WriteContexts returns multiples contexts, 19 | // each for one different channels 20 | func WriteContexts(l *zap.Logger, authToken, botName string, channels []string) map[string]*Context { 21 | chs := map[string]*Context{} 22 | 23 | for _, channel := range channels { 24 | if _, ok := chs[channel]; !ok { 25 | chs[channel] = &Context{ 26 | Logger: l, 27 | ChannelName: channel, 28 | OAuthToken: authToken, 29 | BotName: botName, 30 | } 31 | } 32 | } 33 | 34 | return chs 35 | } 36 | -------------------------------------------------------------------------------- /services/bot/internal/context_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/rafaelbreno/go-bot/test" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func TestContext(t *testing.T) { 11 | tts := []test.TestCases{} 12 | 13 | { 14 | l := zap.NewExample() 15 | chName := []string{"foo", "bar"} 16 | authToken := "bar" 17 | botName := "foobot" 18 | 19 | got := WriteContexts(l, authToken, botName, chName) 20 | 21 | want := map[string]*Context{} 22 | want["foo"] = &Context{ 23 | Logger: l, 24 | ChannelName: "foo", 25 | OAuthToken: authToken, 26 | BotName: botName, 27 | } 28 | want["bar"] = &Context{ 29 | Logger: l, 30 | ChannelName: "bar", 31 | OAuthToken: authToken, 32 | BotName: botName, 33 | } 34 | 35 | tts = append(tts, test.TestCases{ 36 | Name: "WriteContexts", 37 | Want: want, 38 | Got: got, 39 | TestType: test.Equal, 40 | }) 41 | } 42 | 43 | test.RunTests(t, tts) 44 | } 45 | -------------------------------------------------------------------------------- /services/bot/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type TestType int 11 | 12 | const ( 13 | Equal TestType = iota 14 | Nil 15 | Regex 16 | ) 17 | 18 | type TestCases struct { 19 | Name string 20 | Want interface{} 21 | Got interface{} 22 | RegexRule *regexp.Regexp 23 | TestType TestType 24 | } 25 | 26 | func RunTests(t *testing.T, tts []TestCases) { 27 | for _, tt := range tts { 28 | t.Run(tt.Name, func(t *testing.T) { 29 | switch tt.TestType { 30 | case Equal: 31 | assert.Equal(t, tt.Want, tt.Got) 32 | case Regex: 33 | assert.Regexp(t, tt.RegexRule, tt.Got) 34 | case Nil: 35 | assert.Nil(t, tt.Got) 36 | default: 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services/bot/utils/write.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/rafaelbreno/go-bot/internal" 8 | ) 9 | 10 | func Write(ctx *internal.Context, conn net.Conn, msg string) { 11 | ctx.Logger.Info("Sending Message") 12 | _, err := conn.Write([]byte(fmt.Sprintf("%s\r\n", msg))) 13 | if err != nil { 14 | ctx.Logger.Error(err.Error()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /services/message-reader/.env.example: -------------------------------------------------------------------------------- 1 | APP_URL=localhost 2 | APP_PORT=8100 3 | 4 | SENDER_SERVICE_URL=localhost 5 | SENDER_SERVICE_PORT=5004 6 | 7 | BOT_OAUTH_TOKEN= 8 | BOT_NAME= 9 | 10 | REDIS_HOST=localhost 11 | REDIS_PORT=6379 12 | REDIS_NAME=bot-redis 13 | 14 | API_IRC_URL=irc.chat.twitch.tv 15 | API_IRC_PORT=6667 16 | -------------------------------------------------------------------------------- /services/message-reader/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/*.out 2 | .env -------------------------------------------------------------------------------- /services/message-reader/Makefile: -------------------------------------------------------------------------------- 1 | GO=go 2 | GOTEST=$(GO) test 3 | GOCOVER=$(GO) tool cover 4 | .PHONY: test/cover 5 | test/cover: 6 | $(GOTEST) -v -coverprofile=tmp/coverage.out ./... 7 | $(GOCOVER) -func=tmp/coverage.out 8 | 9 | test/cover/html: 10 | $(GOTEST) -v -coverprofile=tmp/coverage.out ./... 11 | $(GOCOVER) -func=tmp/coverage.out 12 | $(GOCOVER) -html=tmp/coverage.out 13 | 14 | -------------------------------------------------------------------------------- /services/message-reader/README.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | - [ ] Storage(interface) 3 | - [ ] Redis 4 | - [ ] On bootstrap, get [channels] from Redis 5 | - [ ] gRPC 6 | - [ ] Add Channel 7 | - [ ] Remove Channel 8 | - [ ] Update Channel 9 | - [ ] Commands 10 | - [ ] Read from Channels 11 | - [ ] Check Redis 12 | - [ ] Parse Message 13 | - [ ] Send to message-sender service via gRPC 14 | - [ ] Channels(struct) 15 | - [ ] 16 | - [ ] 17 | - [ ] 18 | - [ ] 19 | - [ ] 20 | - [ ] 21 | - [ ] 22 | -------------------------------------------------------------------------------- /services/message-reader/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 7 | "github.com/rafaelbreno/go-bot/services/message-reader/reader" 8 | "github.com/rafaelbreno/go-bot/services/message-reader/server" 9 | "github.com/rafaelbreno/go-bot/services/message-reader/storage" 10 | ) 11 | 12 | func main() { 13 | ctx := internal.NewContext() 14 | 15 | ctx.Logger.Info("Starting service...") 16 | 17 | rd := reader.NewReader(ctx, storage.NewRedis(ctx)) 18 | 19 | rd.Start() 20 | 21 | sv := server.NewServer(ctx) 22 | 23 | sv.Start() 24 | 25 | sv.Close() 26 | 27 | fmt.Println("Hello, World!") 28 | } 29 | -------------------------------------------------------------------------------- /services/message-reader/app/main2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "grpc-client/proto" 7 | "time" 8 | 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | func main() { 13 | conn, err := grpc.Dial("localhost:5004", grpc.WithInsecure()) 14 | 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer conn.Close() 19 | 20 | client := proto.NewSenderClient(conn) 21 | 22 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*4) 23 | 24 | defer cancel() 25 | 26 | resp, err := client.SendMessage(ctx, &proto.MessageRequest{ 27 | Msg: "salve fiote", 28 | Channel: "rafiusky", 29 | }) 30 | 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Println(resp) 36 | } 37 | -------------------------------------------------------------------------------- /services/message-reader/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 5 | "github.com/rafaelbreno/go-bot/services/message-reader/proto" 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | type Client struct { 10 | Ctx *internal.Context 11 | Conn *grpc.ClientConn 12 | GRPCClient proto.SenderClient 13 | } 14 | 15 | func NewClient(ctx *internal.Context) *Client { 16 | service_url := string(ctx.Env["SENDER_SERVICE_URL"] + ":" + ctx.Env["SENDER_SERVICE_PORT"]) 17 | 18 | conn, err := grpc.Dial(service_url, grpc.WithInsecure()) 19 | 20 | client := proto.NewSenderClient(conn) 21 | 22 | if err != nil { 23 | ctx.Logger.Fatal(err.Error()) 24 | } 25 | 26 | return &Client{ 27 | Ctx: ctx, 28 | GRPCClient: client, 29 | Conn: conn, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/message-reader/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/rafaelbreno/go-bot/services/message-reader/helpers" 12 | ) 13 | 14 | // Command stores all data related 15 | // to one command 16 | type Command struct { 17 | Trigger string `json:"trigger"` 18 | Answer string `json:"answer"` 19 | Fields []Field `json:"fields"` 20 | SentBy string `json:"sent_by"` 21 | } 22 | 23 | type Field struct { 24 | Key string `json:"key"` 25 | Values []string `json:"values"` 26 | Blacklist []string `json:"blacklist"` 27 | } 28 | 29 | func (c *Command) Parse() string { 30 | fmt.Println("SENT BY:", c.SentBy) 31 | 32 | c.Replace("{sent_by}", c.SentBy) 33 | 34 | for _, field := range c.Fields { 35 | c.Replace(field.Key, c.GetRandom(field.Values, field.Blacklist)) 36 | } 37 | 38 | c.CheckDefault() 39 | 40 | return c.Answer 41 | } 42 | 43 | func (c *Command) Replace(key, value string) { 44 | c.Answer = strings.ReplaceAll(c.Answer, key, value) 45 | } 46 | 47 | var ( 48 | regexRandom = regexp.MustCompile(`{random\.[0-9]+-[0-9]+}`) 49 | regexNumber = regexp.MustCompile("[0-9]+") 50 | ) 51 | 52 | // CheckDefault find and replace default fields 53 | // e.g: {random.1-9} 54 | func (c *Command) CheckDefault() { 55 | randomFields := regexRandom.FindAllString(c.Answer, -1) 56 | fmt.Println(randomFields) 57 | 58 | c.replaceRandomField(randomFields) 59 | } 60 | 61 | // replaceRandomField receives a list with fields 62 | // to parse the max/min number and replace it 63 | // in a string 64 | func (c *Command) replaceRandomField(fields []string) { 65 | for _, k := range fields { 66 | nums := regexNumber.FindAllString(k, -1) 67 | minNum, _ := strconv.Atoi(nums[0]) 68 | maxNum, _ := strconv.Atoi(nums[1]) 69 | if minNum > maxNum { 70 | minNum, maxNum = maxNum, minNum 71 | } 72 | randNum := rand.Intn(maxNum) - minNum 73 | c.Answer = strings.ReplaceAll(c.Answer, k, strconv.Itoa(randNum)) 74 | } 75 | } 76 | 77 | // GetRandom returns a value from a list 78 | // checking if there's a value in a blacklist 79 | func (c *Command) GetRandom(values, blacklist []string) (val string) { 80 | rand.Seed(time.Now().Unix()) 81 | 82 | if len(blacklist) == 0 { 83 | val = values[rand.Intn(len(values))] 84 | return 85 | } 86 | 87 | for { 88 | val = values[rand.Intn(len(values))] 89 | if !helpers.Find(blacklist, val) { 90 | break 91 | } 92 | } 93 | 94 | return 95 | } 96 | -------------------------------------------------------------------------------- /services/message-reader/command/command_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/rafaelbreno/go-bot/services/message-reader/test" 8 | ) 9 | 10 | func TestCommand(t *testing.T) { 11 | tts := []test.TestCases{} 12 | 13 | { 14 | cmd := Command{ 15 | Trigger: "!test", 16 | Answer: "Hello {sent_by}", 17 | SentBy: "foo", 18 | } 19 | 20 | tts = append(tts, test.TestCases{ 21 | Name: "Parse Hello", 22 | Want: "Hello foo", 23 | Got: cmd.Parse(), 24 | TestType: test.Equal, 25 | }) 26 | } 27 | 28 | { 29 | cmd := Command{ 30 | Trigger: "!test", 31 | Answer: "Hello {sent_by}, you're {hero}", 32 | SentBy: "foo", 33 | Fields: []Field{ 34 | { 35 | Key: "{hero}", 36 | Values: []string{"A", "B", "C", "D", "E"}, 37 | Blacklist: []string{}, 38 | }, 39 | }, 40 | } 41 | 42 | tts = append(tts, test.TestCases{ 43 | Name: "Parse Hello", 44 | Got: cmd.Parse(), 45 | RegexRule: regexp.MustCompile(`(Hello foo, you're) (A|B|C|D|E)`), 46 | TestType: test.Regex, 47 | }) 48 | } 49 | 50 | { 51 | cmd := Command{ 52 | Trigger: "!test", 53 | Answer: "Hello {sent_by}, you're {hero}", 54 | SentBy: "foo", 55 | Fields: []Field{ 56 | { 57 | Key: "{hero}", 58 | Values: []string{"A", "B", "C", "D", "E"}, 59 | Blacklist: []string{"A", "C"}, 60 | }, 61 | }, 62 | } 63 | 64 | tts = append(tts, test.TestCases{ 65 | Name: "Parse Hello", 66 | Got: cmd.Parse(), 67 | RegexRule: regexp.MustCompile(`(Hello foo, you're) (B|D|E)`), 68 | TestType: test.Regex, 69 | }) 70 | } 71 | 72 | { 73 | cmd := Command{ 74 | Trigger: "!test", 75 | Answer: "Hello {sent_by}, you're {hero} {random.1-10}", 76 | SentBy: "foo", 77 | Fields: []Field{ 78 | { 79 | Key: "{hero}", 80 | Values: []string{"A", "B", "C", "D", "E"}, 81 | Blacklist: []string{"A", "C"}, 82 | }, 83 | }, 84 | } 85 | 86 | tts = append(tts, test.TestCases{ 87 | Name: "Parse Hello", 88 | Got: cmd.Parse(), 89 | RegexRule: regexp.MustCompile(`(Hello foo, you're) (B|D|E) [1-9]{1,2}`), 90 | TestType: test.Regex, 91 | }) 92 | } 93 | 94 | test.RunTests(t, tts) 95 | } 96 | -------------------------------------------------------------------------------- /services/message-reader/conn/irc.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "net/textproto" 8 | "os" 9 | "time" 10 | 11 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 12 | ) 13 | 14 | type IRC struct { 15 | Conn net.Conn 16 | Ctx *internal.Context 17 | TP *textproto.Reader 18 | Msg chan string 19 | } 20 | 21 | const ( 22 | ircConnURL = `%s:%s` 23 | ) 24 | 25 | func NewIRC(ctx *internal.Context) *IRC { 26 | i := IRC{ 27 | Ctx: ctx, 28 | Msg: make(chan string, 1), 29 | } 30 | i.Listen() 31 | return &i 32 | } 33 | 34 | func (i *IRC) connect() { 35 | connStr := fmt.Sprintf(ircConnURL, i.Ctx.Env["IRC_URL"], i.Ctx.Env["IRC_PORT"]) 36 | 37 | var c net.Conn 38 | var err error 39 | 40 | connected := false 41 | 42 | for tries := 1; tries <= 3; tries++ { 43 | c, err = net.Dial("tcp", connStr) 44 | if err == nil { 45 | i.Conn = c 46 | connected = true 47 | break 48 | } 49 | errMsg := fmt.Sprintf("Error %s. Try number %d!", err.Error(), tries) 50 | i.Ctx.Logger.Error(errMsg) 51 | time.Sleep(2 * time.Second) 52 | } 53 | 54 | if !connected { 55 | i.Ctx.Logger.Error("Unable to connect to IRC") 56 | os.Exit(0) 57 | } 58 | 59 | pass := fmt.Sprintf("PASS %s\r\n", i.Ctx.Env["BOT_OAUTH_TOKEN"]) 60 | if _, err := fmt.Fprint(i.Conn, pass); err != nil { 61 | i.Ctx.Logger.Error(err.Error()) 62 | os.Exit(0) 63 | } 64 | 65 | nick := fmt.Sprintf("NICK %s\r\n", i.Ctx.Env["BOT_NAME"]) 66 | if _, err := fmt.Fprint(i.Conn, nick); err != nil { 67 | i.Ctx.Logger.Error(err.Error()) 68 | os.Exit(0) 69 | } 70 | i.Ctx.Logger.Info(nick) 71 | i.TP = textproto.NewReader(bufio.NewReader(i.Conn)) 72 | } 73 | 74 | // Listen start listen IRC channel, 75 | // and if it disconnects, it will try 76 | // to reconnect 3 times 77 | func (i *IRC) Listen() { 78 | i.connect() 79 | go func() { 80 | for { 81 | msg, err := i.TP.ReadLine() 82 | if err != nil { 83 | close(i.Msg) 84 | i.Ctx.Logger.Error(err.Error()) 85 | i.connect() 86 | continue 87 | } 88 | i.Msg <- msg 89 | } 90 | }() 91 | } 92 | 93 | // Close ends IRC connection 94 | func (i *IRC) Close() { 95 | i.Ctx.Logger.Info("Closing IRC connection") 96 | if err := i.Conn.Close(); err != nil { 97 | i.Ctx.Logger.Error(err.Error()) 98 | os.Exit(0) 99 | } 100 | i.Ctx.Logger.Info("IRC connection closed") 101 | } 102 | -------------------------------------------------------------------------------- /services/message-reader/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rafaelbreno/go-bot/services/message-reader 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.3 7 | github.com/joho/godotenv v1.3.0 8 | github.com/stretchr/testify v1.7.0 9 | go.uber.org/zap v1.19.1 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/golang/protobuf v1.5.2 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | go.uber.org/atomic v1.9.0 // indirect 19 | go.uber.org/multierr v1.7.0 // indirect 20 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect 21 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect 22 | golang.org/x/text v0.3.6 // indirect 23 | google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect 24 | google.golang.org/grpc v1.40.0 // indirect 25 | google.golang.org/protobuf v1.27.1 // indirect 26 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /services/message-reader/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 6 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 7 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 11 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 13 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 14 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 15 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 21 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 22 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 23 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 24 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 25 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 26 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 27 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 28 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 29 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 31 | github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8= 32 | github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc= 33 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 39 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 40 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 41 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 42 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 43 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 44 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 45 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 46 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 47 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 48 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 49 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 50 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 51 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 52 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 53 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 54 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 58 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 59 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 60 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 61 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 62 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 63 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 64 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 65 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 66 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 67 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 68 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 69 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 70 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 71 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 72 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 73 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 74 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 75 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 76 | github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= 77 | github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= 78 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 79 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 83 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 84 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 85 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 87 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 88 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 89 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 91 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 92 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 93 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 94 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 95 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 96 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= 97 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 98 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 99 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 100 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 101 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 102 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 103 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 104 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 105 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 106 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 107 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 108 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 109 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 110 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 111 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 112 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 113 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 114 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 115 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 116 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 117 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 118 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 119 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 120 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 121 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 122 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 123 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 124 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 125 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 126 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 127 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 128 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 129 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 130 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 131 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 151 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 153 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 154 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 155 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 156 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 157 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 158 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 159 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 161 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 162 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 163 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 164 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 165 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 166 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 167 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 169 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 171 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 172 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 173 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 174 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 175 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 176 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 177 | google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 h1:ysnBoUyeL/H6RCvNRhWHjKoDEmguI+mPU+qHgK8qv/w= 178 | google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 179 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 180 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 181 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 182 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 183 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 184 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 185 | google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= 186 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 187 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 188 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 189 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 190 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 191 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 192 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 193 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 194 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 195 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 196 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 197 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 198 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 199 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 201 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 202 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 203 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 204 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 205 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 206 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 207 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 209 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 210 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 211 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 212 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 213 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 214 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 215 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 216 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 217 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 218 | -------------------------------------------------------------------------------- /services/message-reader/helpers/slice.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | func Find(slice []string, val string) bool { 4 | for _, item := range slice { 5 | if item == val { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | 12 | func FindInSliceStr(arr []string, item string) bool { 13 | for _, v := range arr { 14 | if v == item { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func RemoveElementStr(arr []string, item string) []string { 22 | for pos, v := range arr { 23 | if v == item { 24 | return append(arr[0:pos], arr[pos+1:]...) 25 | } 26 | } 27 | return arr 28 | } 29 | -------------------------------------------------------------------------------- /services/message-reader/helpers/slice_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-reader/test" 7 | ) 8 | 9 | func TestSlice(t *testing.T) { 10 | tts := []test.TestCases{} 11 | 12 | { 13 | sl := []string{"A", "B", "C", "D", "E"} 14 | want := true 15 | got := FindInSliceStr(sl, "A") 16 | tts = append(tts, test.TestCases{ 17 | Name: "FindInSliceStr", 18 | Want: want, 19 | Got: got, 20 | TestType: test.Equal, 21 | }) 22 | } 23 | { 24 | sl := []string{"A", "B", "C", "D", "E"} 25 | want := false 26 | got := FindInSliceStr(sl, "X") 27 | tts = append(tts, test.TestCases{ 28 | Name: "FindInSliceStr", 29 | Want: want, 30 | Got: got, 31 | TestType: test.Equal, 32 | }) 33 | } 34 | { 35 | sl := []string{"A", "B", "C", "D", "E"} 36 | want := []string{"A", "B", "C", "D", "E"} 37 | got := RemoveElementStr(sl, "X") 38 | tts = append(tts, test.TestCases{ 39 | Name: "FindInSliceStr", 40 | Want: want, 41 | Got: got, 42 | TestType: test.Equal, 43 | }) 44 | } 45 | { 46 | sl := []string{"A", "B", "C", "D", "E"} 47 | want := []string{"A", "B", "D", "E"} 48 | got := RemoveElementStr(sl, "C") 49 | tts = append(tts, test.TestCases{ 50 | Name: "FindInSliceStr", 51 | Want: want, 52 | Got: got, 53 | TestType: test.Equal, 54 | }) 55 | } 56 | { 57 | sl := []string{"A", "B", "C", "D", "E"} 58 | want := true 59 | got := Find(sl, "C") 60 | tts = append(tts, test.TestCases{ 61 | Name: "FindInSliceStr", 62 | Want: want, 63 | Got: got, 64 | TestType: test.Equal, 65 | }) 66 | } 67 | { 68 | sl := []string{"A", "B", "C", "D", "E"} 69 | want := false 70 | got := Find(sl, "X") 71 | tts = append(tts, test.TestCases{ 72 | Name: "FindInSliceStr", 73 | Want: want, 74 | Got: got, 75 | TestType: test.Equal, 76 | }) 77 | } 78 | 79 | test.RunTests(t, tts) 80 | } 81 | -------------------------------------------------------------------------------- /services/message-reader/internal/context.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/joho/godotenv" 7 | "github.com/rafaelbreno/go-bot/services/message-reader/proto" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type Context struct { 12 | Logger *zap.Logger 13 | Env map[string]string 14 | Msg *chan *proto.MessageRequest 15 | } 16 | 17 | func NewContext() *Context { 18 | l, _ := zap.NewProduction() 19 | 20 | if os.Getenv("APP_ENV") != "prod" { 21 | godotenv.Load() 22 | } 23 | 24 | msgChan := make(chan *proto.MessageRequest) 25 | 26 | return &Context{ 27 | Logger: l, 28 | Env: map[string]string{ 29 | "APP_URL": os.Getenv("APP_URL"), 30 | "APP_PORT": os.Getenv("APP_PORT"), 31 | "BOT_OAUTH_TOKEN": os.Getenv("BOT_OAUTH_TOKEN"), 32 | "BOT_NAME": os.Getenv("BOT_NAME"), 33 | "REDIS_HOST": os.Getenv("REDIS_HOST"), 34 | "REDIS_PORT": os.Getenv("REDIS_PORT"), 35 | "IRC_URL": os.Getenv("API_IRC_URL"), 36 | "IRC_PORT": os.Getenv("API_IRC_PORT"), 37 | "SENDER_SERVICE_URL": os.Getenv("SENDER_SERVICE_URL"), 38 | "SENDER_SERVICE_PORT": os.Getenv("SENDER_SERVICE_PORT"), 39 | }, 40 | Msg: &msgChan, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services/message-reader/message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 7 | "github.com/rafaelbreno/go-bot/services/message-reader/proto" 8 | "github.com/rafaelbreno/go-bot/services/message-reader/storage" 9 | ) 10 | 11 | type Message struct { 12 | Channel string 13 | SentBy string 14 | Value string 15 | } 16 | 17 | var ( 18 | triggerRegex = regexp.MustCompile(`![a-zA-Z0-9]{1,}`) 19 | ) 20 | 21 | func (m *Message) Send(ctx *internal.Context, st *storage.Storage) { 22 | trigger := triggerRegex.FindString(m.Value) 23 | 24 | if trigger == "" { 25 | return 26 | } 27 | 28 | cmd := storage.GetCommand(m.Channel, trigger, *st) 29 | 30 | *ctx.Msg <- &proto.MessageRequest{ 31 | Channel: m.Channel, 32 | Msg: cmd.Parse(), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /services/message-reader/message/parser.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/rafaelbreno/go-bot/services/message-reader/conn" 9 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 10 | ) 11 | 12 | var ( 13 | channelNameRegex = regexp.MustCompile(`#[a-zA-Z0-9_]+`) 14 | ) 15 | 16 | type Parser struct { 17 | Ctx *internal.Context 18 | IRC *conn.IRC 19 | } 20 | 21 | func (p *Parser) Pong() { 22 | p.Ctx.Logger.Info("PONG") 23 | _, err := fmt.Fprint(p.IRC.Conn, "PONG\r\n") 24 | if err != nil { 25 | p.Ctx.Logger.Error(err.Error()) 26 | } 27 | } 28 | 29 | // :ricardinst!ricardinst@ricardinst.tmi.twitch.tv PRIVMSG #rafiusky :e 30 | func (p *Parser) Parse(msg string) *Message { 31 | p.Ctx.Logger.Info(msg) 32 | 33 | if msg[0:4] == "PING" { 34 | p.Pong() 35 | return &Message{} 36 | } 37 | 38 | byteMsg := []byte(msg) 39 | var sentBy []byte 40 | 41 | for _, b := range byteMsg { 42 | if b == byte('!') { 43 | break 44 | } 45 | sentBy = append(sentBy, b) 46 | } 47 | 48 | channel := channelNameRegex.FindString(msg) 49 | 50 | if channel == "" { 51 | return &Message{} 52 | } 53 | 54 | val := strings.SplitN(msg, ":", 3) 55 | 56 | if len(val) < 3 { 57 | return &Message{} 58 | } 59 | 60 | return &Message{ 61 | SentBy: string(sentBy[1:]), 62 | Channel: channel[1:], 63 | Value: val[2], 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /services/message-reader/proto/message.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.17.3 5 | // source: message.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type MessageRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Channel string `protobuf:"bytes,1,opt,name=channel,proto3" json:"channel,omitempty"` 29 | Msg string `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` 30 | } 31 | 32 | func (x *MessageRequest) Reset() { 33 | *x = MessageRequest{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_message_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *MessageRequest) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*MessageRequest) ProtoMessage() {} 46 | 47 | func (x *MessageRequest) ProtoReflect() protoreflect.Message { 48 | mi := &file_message_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use MessageRequest.ProtoReflect.Descriptor instead. 60 | func (*MessageRequest) Descriptor() ([]byte, []int) { 61 | return file_message_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *MessageRequest) GetChannel() string { 65 | if x != nil { 66 | return x.Channel 67 | } 68 | return "" 69 | } 70 | 71 | func (x *MessageRequest) GetMsg() string { 72 | if x != nil { 73 | return x.Msg 74 | } 75 | return "" 76 | } 77 | 78 | type Empty struct { 79 | state protoimpl.MessageState 80 | sizeCache protoimpl.SizeCache 81 | unknownFields protoimpl.UnknownFields 82 | } 83 | 84 | func (x *Empty) Reset() { 85 | *x = Empty{} 86 | if protoimpl.UnsafeEnabled { 87 | mi := &file_message_proto_msgTypes[1] 88 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 89 | ms.StoreMessageInfo(mi) 90 | } 91 | } 92 | 93 | func (x *Empty) String() string { 94 | return protoimpl.X.MessageStringOf(x) 95 | } 96 | 97 | func (*Empty) ProtoMessage() {} 98 | 99 | func (x *Empty) ProtoReflect() protoreflect.Message { 100 | mi := &file_message_proto_msgTypes[1] 101 | if protoimpl.UnsafeEnabled && x != nil { 102 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 103 | if ms.LoadMessageInfo() == nil { 104 | ms.StoreMessageInfo(mi) 105 | } 106 | return ms 107 | } 108 | return mi.MessageOf(x) 109 | } 110 | 111 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 112 | func (*Empty) Descriptor() ([]byte, []int) { 113 | return file_message_proto_rawDescGZIP(), []int{1} 114 | } 115 | 116 | var File_message_proto protoreflect.FileDescriptor 117 | 118 | var file_message_proto_rawDesc = []byte{ 119 | 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 120 | 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3c, 0x0a, 0x0e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 121 | 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 122 | 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 123 | 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 124 | 0x03, 0x6d, 0x73, 0x67, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x3c, 0x0a, 125 | 0x06, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4d, 126 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 127 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 128 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x08, 0x5a, 0x06, 0x2f, 129 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 130 | } 131 | 132 | var ( 133 | file_message_proto_rawDescOnce sync.Once 134 | file_message_proto_rawDescData = file_message_proto_rawDesc 135 | ) 136 | 137 | func file_message_proto_rawDescGZIP() []byte { 138 | file_message_proto_rawDescOnce.Do(func() { 139 | file_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_proto_rawDescData) 140 | }) 141 | return file_message_proto_rawDescData 142 | } 143 | 144 | var file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 145 | var file_message_proto_goTypes = []interface{}{ 146 | (*MessageRequest)(nil), // 0: proto.MessageRequest 147 | (*Empty)(nil), // 1: proto.Empty 148 | } 149 | var file_message_proto_depIdxs = []int32{ 150 | 0, // 0: proto.Sender.SendMessage:input_type -> proto.MessageRequest 151 | 1, // 1: proto.Sender.SendMessage:output_type -> proto.Empty 152 | 1, // [1:2] is the sub-list for method output_type 153 | 0, // [0:1] is the sub-list for method input_type 154 | 0, // [0:0] is the sub-list for extension type_name 155 | 0, // [0:0] is the sub-list for extension extendee 156 | 0, // [0:0] is the sub-list for field type_name 157 | } 158 | 159 | func init() { file_message_proto_init() } 160 | func file_message_proto_init() { 161 | if File_message_proto != nil { 162 | return 163 | } 164 | if !protoimpl.UnsafeEnabled { 165 | file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 166 | switch v := v.(*MessageRequest); i { 167 | case 0: 168 | return &v.state 169 | case 1: 170 | return &v.sizeCache 171 | case 2: 172 | return &v.unknownFields 173 | default: 174 | return nil 175 | } 176 | } 177 | file_message_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 178 | switch v := v.(*Empty); i { 179 | case 0: 180 | return &v.state 181 | case 1: 182 | return &v.sizeCache 183 | case 2: 184 | return &v.unknownFields 185 | default: 186 | return nil 187 | } 188 | } 189 | } 190 | type x struct{} 191 | out := protoimpl.TypeBuilder{ 192 | File: protoimpl.DescBuilder{ 193 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 194 | RawDescriptor: file_message_proto_rawDesc, 195 | NumEnums: 0, 196 | NumMessages: 2, 197 | NumExtensions: 0, 198 | NumServices: 1, 199 | }, 200 | GoTypes: file_message_proto_goTypes, 201 | DependencyIndexes: file_message_proto_depIdxs, 202 | MessageInfos: file_message_proto_msgTypes, 203 | }.Build() 204 | File_message_proto = out.File 205 | file_message_proto_rawDesc = nil 206 | file_message_proto_goTypes = nil 207 | file_message_proto_depIdxs = nil 208 | } 209 | -------------------------------------------------------------------------------- /services/message-reader/proto/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | option go_package = "/proto"; 5 | 6 | service Sender { 7 | rpc SendMessage(MessageRequest) returns (Empty); 8 | } 9 | 10 | message MessageRequest { 11 | string channel = 1; 12 | string msg = 2; 13 | } 14 | 15 | message Empty {} 16 | -------------------------------------------------------------------------------- /services/message-reader/proto/message_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package proto 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // SenderClient is the client API for Sender service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type SenderClient interface { 21 | SendMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*Empty, error) 22 | } 23 | 24 | type senderClient struct { 25 | cc grpc.ClientConnInterface 26 | } 27 | 28 | func NewSenderClient(cc grpc.ClientConnInterface) SenderClient { 29 | return &senderClient{cc} 30 | } 31 | 32 | func (c *senderClient) SendMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*Empty, error) { 33 | out := new(Empty) 34 | err := c.cc.Invoke(ctx, "/proto.Sender/SendMessage", in, out, opts...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return out, nil 39 | } 40 | 41 | // SenderServer is the server API for Sender service. 42 | // All implementations must embed UnimplementedSenderServer 43 | // for forward compatibility 44 | type SenderServer interface { 45 | SendMessage(context.Context, *MessageRequest) (*Empty, error) 46 | mustEmbedUnimplementedSenderServer() 47 | } 48 | 49 | // UnimplementedSenderServer must be embedded to have forward compatible implementations. 50 | type UnimplementedSenderServer struct { 51 | } 52 | 53 | func (UnimplementedSenderServer) SendMessage(context.Context, *MessageRequest) (*Empty, error) { 54 | return nil, status.Errorf(codes.Unimplemented, "method SendMessage not implemented") 55 | } 56 | func (UnimplementedSenderServer) mustEmbedUnimplementedSenderServer() {} 57 | 58 | // UnsafeSenderServer may be embedded to opt out of forward compatibility for this service. 59 | // Use of this interface is not recommended, as added methods to SenderServer will 60 | // result in compilation errors. 61 | type UnsafeSenderServer interface { 62 | mustEmbedUnimplementedSenderServer() 63 | } 64 | 65 | func RegisterSenderServer(s grpc.ServiceRegistrar, srv SenderServer) { 66 | s.RegisterService(&Sender_ServiceDesc, srv) 67 | } 68 | 69 | func _Sender_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 70 | in := new(MessageRequest) 71 | if err := dec(in); err != nil { 72 | return nil, err 73 | } 74 | if interceptor == nil { 75 | return srv.(SenderServer).SendMessage(ctx, in) 76 | } 77 | info := &grpc.UnaryServerInfo{ 78 | Server: srv, 79 | FullMethod: "/proto.Sender/SendMessage", 80 | } 81 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 82 | return srv.(SenderServer).SendMessage(ctx, req.(*MessageRequest)) 83 | } 84 | return interceptor(ctx, in, info, handler) 85 | } 86 | 87 | // Sender_ServiceDesc is the grpc.ServiceDesc for Sender service. 88 | // It's only intended for direct use with grpc.RegisterService, 89 | // and not to be introspected or modified (even as a copy) 90 | var Sender_ServiceDesc = grpc.ServiceDesc{ 91 | ServiceName: "proto.Sender", 92 | HandlerType: (*SenderServer)(nil), 93 | Methods: []grpc.MethodDesc{ 94 | { 95 | MethodName: "SendMessage", 96 | Handler: _Sender_SendMessage_Handler, 97 | }, 98 | }, 99 | Streams: []grpc.StreamDesc{}, 100 | Metadata: "message.proto", 101 | } 102 | -------------------------------------------------------------------------------- /services/message-reader/reader/reader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-reader/conn" 7 | "github.com/rafaelbreno/go-bot/services/message-reader/helpers" 8 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 9 | "github.com/rafaelbreno/go-bot/services/message-reader/message" 10 | "github.com/rafaelbreno/go-bot/services/message-reader/storage" 11 | ) 12 | 13 | type Reader struct { 14 | Channels []string 15 | Storage storage.Storage 16 | IRC *conn.IRC 17 | Ctx *internal.Context 18 | } 19 | 20 | func NewReader(ctx *internal.Context, st storage.Storage) *Reader { 21 | irc := conn.NewIRC(ctx) 22 | 23 | return &Reader{ 24 | Channels: []string{}, 25 | Storage: st, 26 | Ctx: ctx, 27 | IRC: irc, 28 | } 29 | } 30 | 31 | func (r *Reader) Start() { 32 | r.joinRedis() 33 | p := message.Parser{ 34 | Ctx: r.Ctx, 35 | IRC: r.IRC, 36 | } 37 | 38 | go func() { 39 | for { 40 | select { 41 | case msg := <-r.IRC.Msg: 42 | r.Ctx.Logger.Info(fmt.Sprintf("received %s", msg)) 43 | m := p.Parse(msg) 44 | go m.Send(r.Ctx, &r.Storage) 45 | } 46 | } 47 | }() 48 | } 49 | 50 | func (r *Reader) joinRedis() { 51 | for _, c := range storage.GetChannels(r.Storage) { 52 | r.Channels = append(r.Channels, c) 53 | r.JoinChat(c) 54 | } 55 | } 56 | 57 | func (r *Reader) CheckChannel(name string) { 58 | if !helpers.FindInSliceStr(r.Channels, name) { 59 | r.Channels = append(r.Channels, name) 60 | r.JoinChat(name) 61 | } 62 | } 63 | 64 | func (r *Reader) JoinChat(name string) { 65 | r.write(fmt.Sprintf("JOIN #%s", name)) 66 | } 67 | 68 | func (r *Reader) partChat(name string) { 69 | r.write(fmt.Sprintf("PART #%s", name)) 70 | r.Channels = helpers.RemoveElementStr(r.Channels, name) 71 | } 72 | 73 | func (r *Reader) write(msg string) { 74 | r.Ctx.Logger.Info("Sending Message") 75 | _, err := r.IRC.Conn.Write([]byte(fmt.Sprintf("%s\r\n", msg))) 76 | if err != nil { 77 | r.Ctx.Logger.Error(err.Error()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /services/message-reader/repository/message.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/rafaelbreno/go-bot/services/message-reader/conn" 5 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 6 | "github.com/rafaelbreno/go-bot/services/message-reader/proto" 7 | "github.com/rafaelbreno/go-bot/services/message-reader/reader" 8 | "github.com/rafaelbreno/go-bot/services/message-reader/sender" 9 | ) 10 | 11 | type MessageRepo interface { 12 | SendMessage(msg *proto.MessageRequest) *proto.Empty 13 | } 14 | 15 | // MessageRepoCtx handles actions 16 | // related to messages 17 | type MessageRepoCtx struct { 18 | Ctx *internal.Context 19 | Reader *reader.Reader 20 | } 21 | 22 | // SendMessage receives a message 23 | // and send it to a channel's chat 24 | func (m *MessageRepoCtx) SendMessage(msg *proto.MessageRequest) *proto.Empty { 25 | if msg.Msg == "" { 26 | m.Ctx.Logger.Error("empty 'msg' field") 27 | return &proto.Empty{} 28 | } 29 | if msg.Channel == "" { 30 | m.Ctx.Logger.Error("empty 'channel' field") 31 | return &proto.Empty{} 32 | } 33 | 34 | m.Reader..SendMessage(msg.Channel, msg.Msg) 35 | 36 | return &proto.Empty{} 37 | } 38 | 39 | // NewMessageRepo builds a message repository 40 | // with given context 41 | func NewMessageRepo(ctx *internal.Context) *MessageRepoCtx { 42 | return &MessageRepoCtx{ 43 | Ctx: ctx, 44 | Sender: sender.NewSender(ctx, conn.NewIRC(ctx)), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /services/message-reader/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/rafaelbreno/go-bot/services/message-reader/client" 8 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 9 | "github.com/rafaelbreno/go-bot/services/message-reader/proto" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | type Server struct { 14 | Client *client.Client 15 | Ctx *internal.Context 16 | } 17 | 18 | func NewServer(ctx *internal.Context) *Server { 19 | client := client.NewClient(ctx) 20 | 21 | return &Server{ 22 | Client: client, 23 | Ctx: ctx, 24 | } 25 | } 26 | 27 | func (s *Server) Start() { 28 | for { 29 | select { 30 | case msg := <-*s.Ctx.Msg: 31 | SendMessage(msg) 32 | //fmt.Println(msg.Channel) 33 | //fmt.Println(msg.Msg) 34 | } 35 | } 36 | } 37 | 38 | func SendMessage(msg *proto.MessageRequest) { 39 | conn, err := grpc.Dial("localhost:5004", grpc.WithInsecure()) 40 | 41 | if err != nil { 42 | panic(err) 43 | } 44 | defer conn.Close() 45 | 46 | client := proto.NewSenderClient(conn) 47 | 48 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*4) 49 | 50 | defer cancel() 51 | 52 | _, err = client.SendMessage(ctx, msg) 53 | 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | func (s *Server) Close() { 60 | s.Client.Conn.Close() 61 | } 62 | -------------------------------------------------------------------------------- /services/message-reader/storage/redis.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | redis "github.com/go-redis/redis/v8" 9 | "github.com/rafaelbreno/go-bot/services/message-reader/command" 10 | "github.com/rafaelbreno/go-bot/services/message-reader/internal" 11 | ) 12 | 13 | type Redis struct { 14 | Conn *redis.Client 15 | Ctx *internal.Context 16 | } 17 | 18 | // NewRedis create a new Redis instance 19 | func NewRedis(ctx *internal.Context) *Redis { 20 | client := redis.NewClient(&redis.Options{ 21 | Addr: fmt.Sprintf("%s:%s", ctx.Env["REDIS_HOST"], ctx.Env["REDIS_PORT"]), 22 | Password: "", // no password set 23 | DB: 0, // use default DB 24 | }) 25 | return &Redis{ 26 | Conn: client, 27 | Ctx: ctx, 28 | } 29 | } 30 | 31 | // GetChannels retrieves from Redis a list 32 | // of all channels 33 | func (r *Redis) GetChannels(key string) []string { 34 | var channels struct { 35 | Chs []string `json:"channels"` 36 | } 37 | 38 | res, err := r.Conn.Get(context.Background(), key).Result() 39 | 40 | if err != nil { 41 | r.Ctx.Logger.Error(err.Error()) 42 | return channels.Chs 43 | } 44 | 45 | err = json.Unmarshal([]byte(res), &channels) 46 | 47 | if err != nil { 48 | r.Ctx.Logger.Error(err.Error()) 49 | return channels.Chs 50 | } 51 | 52 | return channels.Chs 53 | } 54 | 55 | // GetCommand retrieves from Redis a command 56 | // from a given channel and key 57 | func (r *Redis) GetCommand(key string) command.Command { 58 | var cmd command.Command 59 | 60 | res, err := r.Conn.Get(context.Background(), key).Result() 61 | 62 | if err != nil { 63 | r.Ctx.Logger.Error(err.Error()) 64 | return cmd 65 | } 66 | 67 | err = json.Unmarshal([]byte(res), &cmd) 68 | 69 | if err != nil { 70 | r.Ctx.Logger.Error(err.Error()) 71 | return cmd 72 | } 73 | 74 | return cmd 75 | } 76 | -------------------------------------------------------------------------------- /services/message-reader/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | 6 | command "github.com/rafaelbreno/go-bot/services/message-reader/command" 7 | ) 8 | 9 | // Storage consumes a in-memory database 10 | // redis, Memcached, DynamoDB 11 | type Storage interface { 12 | GetChannels(key string) []string 13 | GetCommand(key string) command.Command 14 | } 15 | 16 | const ( 17 | channelsKey = "[channels]" 18 | commandKey = "[%s][%s]" 19 | ) 20 | 21 | // GetChannels retrieve channels list 22 | // from a Storage 23 | func GetChannels(s Storage) []string { 24 | return s.GetChannels(channelsKey) 25 | } 26 | 27 | // GetCommand retrieve command from Redis 28 | func GetCommand(channel, key string, s Storage) command.Command { 29 | if channel == "" || key == "" { 30 | return command.Command{} 31 | } 32 | 33 | redisKey := fmt.Sprintf(commandKey, channel, key) 34 | 35 | cmd := s.GetCommand(redisKey) 36 | 37 | return cmd 38 | } 39 | -------------------------------------------------------------------------------- /services/message-reader/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type TestType int 11 | 12 | const ( 13 | Equal TestType = iota 14 | Nil 15 | Regex 16 | ) 17 | 18 | type TestCases struct { 19 | Name string 20 | Want interface{} 21 | Got interface{} 22 | RegexRule *regexp.Regexp 23 | TestType TestType 24 | } 25 | 26 | func RunTests(t *testing.T, tts []TestCases) { 27 | for _, tt := range tts { 28 | t.Run(tt.Name, func(t *testing.T) { 29 | switch tt.TestType { 30 | case Equal: 31 | assert.Equal(t, tt.Want, tt.Got) 32 | case Regex: 33 | assert.Regexp(t, tt.RegexRule, tt.Got) 34 | case Nil: 35 | assert.Nil(t, tt.Got) 36 | default: 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services/message-reader/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelbreno/go-bot/8c31608f44a21a3d5ac5527b6fa20cda50f5ee2e/services/message-reader/tmp/.gitkeep -------------------------------------------------------------------------------- /services/message-sender/.env.example: -------------------------------------------------------------------------------- 1 | APP_URL=localhost 2 | APP_PORT=8100 3 | 4 | BOT_OAUTH_TOKEN= 5 | BOT_NAME= 6 | 7 | API_IRC_URL=irc.chat.twitch.tv 8 | API_IRC_PORT=6667 9 | -------------------------------------------------------------------------------- /services/message-sender/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/*.out 2 | .env 3 | -------------------------------------------------------------------------------- /services/message-sender/Makefile: -------------------------------------------------------------------------------- 1 | GO=go 2 | GOTEST=$(GO) test 3 | GOCOVER=$(GO) tool cover 4 | .PHONY: test/cover 5 | test/cover: 6 | $(GOTEST) -v -coverprofile=tmp/coverage.out ./... 7 | $(GOCOVER) -func=tmp/coverage.out 8 | 9 | test/cover/html: 10 | $(GOTEST) -v -coverprofile=tmp/coverage.out ./... 11 | $(GOCOVER) -func=tmp/coverage.out 12 | $(GOCOVER) -html=tmp/coverage.out 13 | 14 | -------------------------------------------------------------------------------- /services/message-sender/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-sender/internal" 7 | "github.com/rafaelbreno/go-bot/services/message-sender/proto" 8 | "github.com/rafaelbreno/go-bot/services/message-sender/server" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | func main() { 13 | lis, err := net.Listen("tcp", "localhost:5004") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | var opts []grpc.ServerOption 19 | 20 | grpcServer := grpc.NewServer(opts...) 21 | 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | sv := server.NewServer(internal.NewContext()) 27 | 28 | proto.RegisterSenderServer(grpcServer, sv) 29 | 30 | grpcServer.Serve(lis) 31 | } 32 | -------------------------------------------------------------------------------- /services/message-sender/conn/irc.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "net/textproto" 8 | "os" 9 | "time" 10 | 11 | "github.com/rafaelbreno/go-bot/services/message-sender/internal" 12 | ) 13 | 14 | type IRC struct { 15 | Conn net.Conn 16 | Ctx *internal.Context 17 | connFlag chan bool 18 | } 19 | 20 | const ( 21 | ircConnURL = `%s:%s` 22 | ) 23 | 24 | func NewIRC(ctx *internal.Context) *IRC { 25 | i := IRC{ 26 | Ctx: ctx, 27 | } 28 | i.connect() 29 | go i.pong() 30 | return &i 31 | } 32 | 33 | func (i *IRC) pong() { 34 | tp := textproto.NewReader(bufio.NewReader((i.Conn))) 35 | 36 | for { 37 | msg, _ := tp.ReadLine() 38 | if len(msg) > 3 { 39 | if msg[:4] == "PING" { 40 | fmt.Fprint(i.Conn, "PONG") 41 | i.Ctx.Logger.Info("PONG") 42 | } 43 | } 44 | } 45 | } 46 | 47 | func (i *IRC) connect() { 48 | connStr := fmt.Sprintf(ircConnURL, i.Ctx.Env["IRC_URL"], i.Ctx.Env["IRC_PORT"]) 49 | 50 | var c net.Conn 51 | var err error 52 | 53 | connected := false 54 | 55 | for tries := 1; tries <= 3; tries++ { 56 | c, err = net.Dial("tcp", connStr) 57 | if err == nil { 58 | i.Conn = c 59 | connected = true 60 | break 61 | } 62 | errMsg := fmt.Sprintf("Error %s. Try number %d!", err.Error(), tries) 63 | i.Ctx.Logger.Error(errMsg) 64 | time.Sleep(2 * time.Second) 65 | } 66 | 67 | if !connected { 68 | i.Ctx.Logger.Error("Unable to connect to IRC") 69 | os.Exit(0) 70 | } 71 | 72 | pass := fmt.Sprintf("PASS %s\r\n", i.Ctx.Env["BOT_OAUTH_TOKEN"]) 73 | if _, err := fmt.Fprint(i.Conn, pass); err != nil { 74 | i.Ctx.Logger.Error(err.Error()) 75 | os.Exit(0) 76 | } 77 | 78 | nick := fmt.Sprintf("NICK %s\r\n", i.Ctx.Env["BOT_NAME"]) 79 | if _, err := fmt.Fprint(i.Conn, nick); err != nil { 80 | i.Ctx.Logger.Error(err.Error()) 81 | os.Exit(0) 82 | } 83 | i.Ctx.Logger.Info(nick) 84 | } 85 | 86 | // Close ends IRC connection 87 | func (i *IRC) Close() { 88 | i.Ctx.Logger.Info("Closing IRC connection") 89 | if err := i.Conn.Close(); err != nil { 90 | i.Ctx.Logger.Error(err.Error()) 91 | os.Exit(0) 92 | } 93 | i.Ctx.Logger.Info("IRC connection closed") 94 | } 95 | -------------------------------------------------------------------------------- /services/message-sender/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rafaelbreno/go-bot/services/message-sender 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/joho/godotenv v1.3.0 7 | github.com/stretchr/testify v1.7.0 8 | go.uber.org/zap v1.19.1 9 | google.golang.org/grpc v1.40.0 10 | google.golang.org/protobuf v1.27.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/golang/protobuf v1.5.2 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | go.uber.org/atomic v1.9.0 // indirect 18 | go.uber.org/multierr v1.7.0 // indirect 19 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect 20 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect 21 | golang.org/x/text v0.3.6 // indirect 22 | google.golang.org/genproto v0.0.0-20210916144049-3192f974c780 // indirect 23 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /services/message-sender/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 6 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 7 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 8 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 12 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 13 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 18 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 19 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 20 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 21 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 22 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 23 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 24 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 25 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 29 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 30 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 31 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 32 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 33 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 34 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 35 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 36 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 37 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 38 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 39 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 40 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 41 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 43 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 46 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 47 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 48 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 49 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 50 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 51 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 52 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 53 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 54 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 57 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 61 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 62 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 65 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 66 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 67 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 69 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 70 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 71 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 72 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 73 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= 74 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 75 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 76 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 77 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 78 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 79 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 82 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 83 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 84 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 85 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 86 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 87 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 88 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 89 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 90 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 91 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 92 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 93 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 95 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 96 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 99 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= 100 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 101 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 102 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 103 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 115 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 117 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 118 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 119 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 120 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 121 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 122 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 123 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 124 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 125 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 126 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 127 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 128 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 129 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 130 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 134 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 136 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 137 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 138 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 139 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 140 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 141 | google.golang.org/genproto v0.0.0-20210916144049-3192f974c780 h1:RE6jTVCXBKZ7U9atSg8N3bsjRvvUujhEPspbEhdyy8s= 142 | google.golang.org/genproto v0.0.0-20210916144049-3192f974c780/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 143 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 144 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 145 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 146 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 147 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 148 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 149 | google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= 150 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 151 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 152 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 153 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 154 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 155 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 156 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 157 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 158 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 159 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 160 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 161 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 162 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 163 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 166 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 168 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 169 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 170 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 171 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 172 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 173 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 174 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 175 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 176 | -------------------------------------------------------------------------------- /services/message-sender/helpers/slice.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | func FindInSliceStr(arr []string, item string) bool { 4 | for _, v := range arr { 5 | if v == item { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | 12 | func RemoveElementStr(arr []string, item string) []string { 13 | for pos, v := range arr { 14 | if v == item { 15 | return append(arr[0:pos], arr[pos+1:]...) 16 | } 17 | } 18 | return arr 19 | } 20 | -------------------------------------------------------------------------------- /services/message-sender/helpers/slice_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-sender/test" 7 | ) 8 | 9 | func TestSlice(t *testing.T) { 10 | tts := []test.TestCases{} 11 | 12 | { 13 | sl := []string{"A", "B", "C", "D", "E"} 14 | want := true 15 | got := FindInSliceStr(sl, "A") 16 | tts = append(tts, test.TestCases{ 17 | Name: "FindInSliceStr", 18 | Want: want, 19 | Got: got, 20 | TestType: test.Equal, 21 | }) 22 | } 23 | { 24 | sl := []string{"A", "B", "C", "D", "E"} 25 | want := false 26 | got := FindInSliceStr(sl, "X") 27 | tts = append(tts, test.TestCases{ 28 | Name: "FindInSliceStr", 29 | Want: want, 30 | Got: got, 31 | TestType: test.Equal, 32 | }) 33 | } 34 | { 35 | sl := []string{"A", "B", "C", "D", "E"} 36 | want := []string{"A", "B", "C", "D", "E"} 37 | got := RemoveElementStr(sl, "X") 38 | tts = append(tts, test.TestCases{ 39 | Name: "FindInSliceStr", 40 | Want: want, 41 | Got: got, 42 | TestType: test.Equal, 43 | }) 44 | } 45 | { 46 | sl := []string{"A", "B", "C", "D", "E"} 47 | want := []string{"A", "B", "D", "E"} 48 | got := RemoveElementStr(sl, "C") 49 | tts = append(tts, test.TestCases{ 50 | Name: "FindInSliceStr", 51 | Want: want, 52 | Got: got, 53 | TestType: test.Equal, 54 | }) 55 | } 56 | 57 | test.RunTests(t, tts) 58 | } 59 | -------------------------------------------------------------------------------- /services/message-sender/internal/context.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/joho/godotenv" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type Context struct { 11 | Logger *zap.Logger 12 | Env map[string]string 13 | } 14 | 15 | func NewContext() *Context { 16 | if err := godotenv.Load(); err != nil { 17 | panic(err) 18 | } 19 | l, _ := zap.NewProduction() 20 | 21 | c := Context{ 22 | Env: map[string]string{ 23 | "IRC_URL": os.Getenv("API_IRC_URL"), 24 | "IRC_PORT": os.Getenv("API_IRC_PORT"), 25 | "BOT_OAUTH_TOKEN": os.Getenv("BOT_OAUTH_TOKEN"), 26 | "BOT_NAME": os.Getenv("BOT_NAME"), 27 | }, 28 | Logger: l, 29 | } 30 | 31 | return &c 32 | } 33 | -------------------------------------------------------------------------------- /services/message-sender/proto/message.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.17.3 5 | // source: message.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type MessageRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Channel string `protobuf:"bytes,1,opt,name=channel,proto3" json:"channel,omitempty"` 29 | Msg string `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` 30 | } 31 | 32 | func (x *MessageRequest) Reset() { 33 | *x = MessageRequest{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_message_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *MessageRequest) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*MessageRequest) ProtoMessage() {} 46 | 47 | func (x *MessageRequest) ProtoReflect() protoreflect.Message { 48 | mi := &file_message_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use MessageRequest.ProtoReflect.Descriptor instead. 60 | func (*MessageRequest) Descriptor() ([]byte, []int) { 61 | return file_message_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *MessageRequest) GetChannel() string { 65 | if x != nil { 66 | return x.Channel 67 | } 68 | return "" 69 | } 70 | 71 | func (x *MessageRequest) GetMsg() string { 72 | if x != nil { 73 | return x.Msg 74 | } 75 | return "" 76 | } 77 | 78 | type Empty struct { 79 | state protoimpl.MessageState 80 | sizeCache protoimpl.SizeCache 81 | unknownFields protoimpl.UnknownFields 82 | } 83 | 84 | func (x *Empty) Reset() { 85 | *x = Empty{} 86 | if protoimpl.UnsafeEnabled { 87 | mi := &file_message_proto_msgTypes[1] 88 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 89 | ms.StoreMessageInfo(mi) 90 | } 91 | } 92 | 93 | func (x *Empty) String() string { 94 | return protoimpl.X.MessageStringOf(x) 95 | } 96 | 97 | func (*Empty) ProtoMessage() {} 98 | 99 | func (x *Empty) ProtoReflect() protoreflect.Message { 100 | mi := &file_message_proto_msgTypes[1] 101 | if protoimpl.UnsafeEnabled && x != nil { 102 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 103 | if ms.LoadMessageInfo() == nil { 104 | ms.StoreMessageInfo(mi) 105 | } 106 | return ms 107 | } 108 | return mi.MessageOf(x) 109 | } 110 | 111 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 112 | func (*Empty) Descriptor() ([]byte, []int) { 113 | return file_message_proto_rawDescGZIP(), []int{1} 114 | } 115 | 116 | var File_message_proto protoreflect.FileDescriptor 117 | 118 | var file_message_proto_rawDesc = []byte{ 119 | 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 120 | 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3c, 0x0a, 0x0e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 121 | 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 122 | 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 123 | 0x65, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 124 | 0x03, 0x6d, 0x73, 0x67, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x3e, 0x0a, 125 | 0x06, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4d, 126 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 127 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 128 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x08, 0x5a, 129 | 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 130 | } 131 | 132 | var ( 133 | file_message_proto_rawDescOnce sync.Once 134 | file_message_proto_rawDescData = file_message_proto_rawDesc 135 | ) 136 | 137 | func file_message_proto_rawDescGZIP() []byte { 138 | file_message_proto_rawDescOnce.Do(func() { 139 | file_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_proto_rawDescData) 140 | }) 141 | return file_message_proto_rawDescData 142 | } 143 | 144 | var file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 145 | var file_message_proto_goTypes = []interface{}{ 146 | (*MessageRequest)(nil), // 0: proto.MessageRequest 147 | (*Empty)(nil), // 1: proto.Empty 148 | } 149 | var file_message_proto_depIdxs = []int32{ 150 | 0, // 0: proto.Sender.SendMessage:input_type -> proto.MessageRequest 151 | 1, // 1: proto.Sender.SendMessage:output_type -> proto.Empty 152 | 1, // [1:2] is the sub-list for method output_type 153 | 0, // [0:1] is the sub-list for method input_type 154 | 0, // [0:0] is the sub-list for extension type_name 155 | 0, // [0:0] is the sub-list for extension extendee 156 | 0, // [0:0] is the sub-list for field type_name 157 | } 158 | 159 | func init() { file_message_proto_init() } 160 | func file_message_proto_init() { 161 | if File_message_proto != nil { 162 | return 163 | } 164 | if !protoimpl.UnsafeEnabled { 165 | file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 166 | switch v := v.(*MessageRequest); i { 167 | case 0: 168 | return &v.state 169 | case 1: 170 | return &v.sizeCache 171 | case 2: 172 | return &v.unknownFields 173 | default: 174 | return nil 175 | } 176 | } 177 | file_message_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 178 | switch v := v.(*Empty); i { 179 | case 0: 180 | return &v.state 181 | case 1: 182 | return &v.sizeCache 183 | case 2: 184 | return &v.unknownFields 185 | default: 186 | return nil 187 | } 188 | } 189 | } 190 | type x struct{} 191 | out := protoimpl.TypeBuilder{ 192 | File: protoimpl.DescBuilder{ 193 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 194 | RawDescriptor: file_message_proto_rawDesc, 195 | NumEnums: 0, 196 | NumMessages: 2, 197 | NumExtensions: 0, 198 | NumServices: 1, 199 | }, 200 | GoTypes: file_message_proto_goTypes, 201 | DependencyIndexes: file_message_proto_depIdxs, 202 | MessageInfos: file_message_proto_msgTypes, 203 | }.Build() 204 | File_message_proto = out.File 205 | file_message_proto_rawDesc = nil 206 | file_message_proto_goTypes = nil 207 | file_message_proto_depIdxs = nil 208 | } 209 | -------------------------------------------------------------------------------- /services/message-sender/proto/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | option go_package = "/proto"; 5 | 6 | service Sender { 7 | rpc SendMessage(MessageRequest) returns (Empty) {} 8 | } 9 | 10 | message MessageRequest { 11 | string channel = 1; 12 | string msg = 2; 13 | } 14 | 15 | message Empty {} 16 | -------------------------------------------------------------------------------- /services/message-sender/proto/message_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package proto 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // SenderClient is the client API for Sender service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type SenderClient interface { 21 | SendMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*Empty, error) 22 | } 23 | 24 | type senderClient struct { 25 | cc grpc.ClientConnInterface 26 | } 27 | 28 | func NewSenderClient(cc grpc.ClientConnInterface) SenderClient { 29 | return &senderClient{cc} 30 | } 31 | 32 | func (c *senderClient) SendMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*Empty, error) { 33 | out := new(Empty) 34 | err := c.cc.Invoke(ctx, "/proto.Sender/SendMessage", in, out, opts...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return out, nil 39 | } 40 | 41 | // SenderServer is the server API for Sender service. 42 | // All implementations must embed UnimplementedSenderServer 43 | // for forward compatibility 44 | type SenderServer interface { 45 | SendMessage(context.Context, *MessageRequest) (*Empty, error) 46 | mustEmbedUnimplementedSenderServer() 47 | } 48 | 49 | // UnimplementedSenderServer must be embedded to have forward compatible implementations. 50 | type UnimplementedSenderServer struct { 51 | } 52 | 53 | func (UnimplementedSenderServer) SendMessage(context.Context, *MessageRequest) (*Empty, error) { 54 | return nil, status.Errorf(codes.Unimplemented, "method SendMessage not implemented") 55 | } 56 | func (UnimplementedSenderServer) mustEmbedUnimplementedSenderServer() {} 57 | 58 | // UnsafeSenderServer may be embedded to opt out of forward compatibility for this service. 59 | // Use of this interface is not recommended, as added methods to SenderServer will 60 | // result in compilation errors. 61 | type UnsafeSenderServer interface { 62 | mustEmbedUnimplementedSenderServer() 63 | } 64 | 65 | func RegisterSenderServer(s grpc.ServiceRegistrar, srv SenderServer) { 66 | s.RegisterService(&Sender_ServiceDesc, srv) 67 | } 68 | 69 | func _Sender_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 70 | in := new(MessageRequest) 71 | if err := dec(in); err != nil { 72 | return nil, err 73 | } 74 | if interceptor == nil { 75 | return srv.(SenderServer).SendMessage(ctx, in) 76 | } 77 | info := &grpc.UnaryServerInfo{ 78 | Server: srv, 79 | FullMethod: "/proto.Sender/SendMessage", 80 | } 81 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 82 | return srv.(SenderServer).SendMessage(ctx, req.(*MessageRequest)) 83 | } 84 | return interceptor(ctx, in, info, handler) 85 | } 86 | 87 | // Sender_ServiceDesc is the grpc.ServiceDesc for Sender service. 88 | // It's only intended for direct use with grpc.RegisterService, 89 | // and not to be introspected or modified (even as a copy) 90 | var Sender_ServiceDesc = grpc.ServiceDesc{ 91 | ServiceName: "proto.Sender", 92 | HandlerType: (*SenderServer)(nil), 93 | Methods: []grpc.MethodDesc{ 94 | { 95 | MethodName: "SendMessage", 96 | Handler: _Sender_SendMessage_Handler, 97 | }, 98 | }, 99 | Streams: []grpc.StreamDesc{}, 100 | Metadata: "message.proto", 101 | } 102 | -------------------------------------------------------------------------------- /services/message-sender/repository/message.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/rafaelbreno/go-bot/services/message-sender/conn" 5 | "github.com/rafaelbreno/go-bot/services/message-sender/internal" 6 | "github.com/rafaelbreno/go-bot/services/message-sender/proto" 7 | "github.com/rafaelbreno/go-bot/services/message-sender/sender" 8 | ) 9 | 10 | type MessageRepo interface { 11 | SendMessage(msg *proto.MessageRequest) *proto.Empty 12 | } 13 | 14 | // MessageRepoCtx handles actions 15 | // related to messages 16 | type MessageRepoCtx struct { 17 | Ctx *internal.Context 18 | Sender *sender.Sender 19 | } 20 | 21 | // SendMessage receives a message 22 | // and send it to a channel's chat 23 | func (m *MessageRepoCtx) SendMessage(msg *proto.MessageRequest) *proto.Empty { 24 | if msg.Msg == "" { 25 | m.Ctx.Logger.Error("empty 'msg' field") 26 | return &proto.Empty{} 27 | } 28 | if msg.Channel == "" { 29 | m.Ctx.Logger.Error("empty 'channel' field") 30 | return &proto.Empty{} 31 | } 32 | 33 | m.Sender.SendMessage(msg.Channel, msg.Msg) 34 | 35 | return &proto.Empty{} 36 | } 37 | 38 | // NewMessageRepo builds a message repository 39 | // with given context 40 | func NewMessageRepo(ctx *internal.Context) *MessageRepoCtx { 41 | return &MessageRepoCtx{ 42 | Ctx: ctx, 43 | Sender: sender.NewSender(ctx, conn.NewIRC(ctx)), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /services/message-sender/sender/sender.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-sender/conn" 7 | "github.com/rafaelbreno/go-bot/services/message-sender/helpers" 8 | "github.com/rafaelbreno/go-bot/services/message-sender/internal" 9 | ) 10 | 11 | // Sender manages the channel that are connected 12 | type Sender struct { 13 | Channels []string 14 | Ctx *internal.Context 15 | IRC *conn.IRC 16 | } 17 | 18 | // NewSender creates an instance for Sender struct 19 | func NewSender(ctx *internal.Context, irc *conn.IRC) *Sender { 20 | return &Sender{ 21 | Channels: []string{}, 22 | Ctx: ctx, 23 | IRC: irc, 24 | } 25 | } 26 | 27 | // CheckChannel verify if there's a channel already connected 28 | func (s *Sender) CheckChannel(name string) { 29 | if !helpers.FindInSliceStr(s.Channels, name) { 30 | s.Channels = append(s.Channels, name) 31 | s.joinChat(name) 32 | } 33 | } 34 | 35 | // SendMessage 36 | func (s *Sender) SendMessage(channel, message string) { 37 | s.CheckChannel(channel) 38 | s.write(fmt.Sprintf("PRIVMSG #%s :%s", channel, message)) 39 | } 40 | 41 | func (s *Sender) joinChat(name string) { 42 | s.write(fmt.Sprintf("JOIN #%s", name)) 43 | } 44 | 45 | func (s *Sender) partChat(name string) { 46 | s.write(fmt.Sprintf("PART #%s", name)) 47 | s.Channels = helpers.RemoveElementStr(s.Channels, name) 48 | } 49 | 50 | func (s *Sender) write(msg string) { 51 | s.Ctx.Logger.Info("Sending Message") 52 | _, err := s.IRC.Conn.Write([]byte(fmt.Sprintf("%s\r\n", msg))) 53 | if err != nil { 54 | s.Ctx.Logger.Error(err.Error()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /services/message-sender/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rafaelbreno/go-bot/services/message-sender/internal" 7 | "github.com/rafaelbreno/go-bot/services/message-sender/proto" 8 | "github.com/rafaelbreno/go-bot/services/message-sender/repository" 9 | ) 10 | 11 | // Server handles gRPC connection 12 | type Server struct { 13 | proto.UnimplementedSenderServer 14 | Ctx *internal.Context 15 | Repo *repository.MessageRepoCtx 16 | } 17 | 18 | // SendMessage receives a message body to treat 19 | // and send to a channel's chat 20 | func (s *Server) SendMessage(ctx context.Context, msg *proto.MessageRequest) (*proto.Empty, error) { 21 | return s.Repo.SendMessage(msg), nil 22 | } 23 | 24 | func NewServer(ctx *internal.Context) *Server { 25 | return &Server{ 26 | Ctx: ctx, 27 | Repo: repository.NewMessageRepo(ctx), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/message-sender/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type TestType int 11 | 12 | const ( 13 | Equal TestType = iota 14 | Nil 15 | Regex 16 | ) 17 | 18 | type TestCases struct { 19 | Name string 20 | Want interface{} 21 | Got interface{} 22 | RegexRule *regexp.Regexp 23 | TestType TestType 24 | } 25 | 26 | func RunTests(t *testing.T, tts []TestCases) { 27 | for _, tt := range tts { 28 | t.Run(tt.Name, func(t *testing.T) { 29 | switch tt.TestType { 30 | case Equal: 31 | assert.Equal(t, tt.Want, tt.Got) 32 | case Regex: 33 | assert.Regexp(t, tt.RegexRule, tt.Got) 34 | case Nil: 35 | assert.Nil(t, tt.Got) 36 | default: 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services/message-sender/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafaelbreno/go-bot/8c31608f44a21a3d5ac5527b6fa20cda50f5ee2e/services/message-sender/tmp/.gitkeep -------------------------------------------------------------------------------- /services/queue-mgr/.env.example: -------------------------------------------------------------------------------- 1 | REDIS_HOST=localhost 2 | REDIS_PORT=6379 3 | REDIS_NAME=bot-redis 4 | 5 | KAFKA_URL=localhost 6 | KAFKA_PORT=9092 7 | KAFKA_TOPIC=commands 8 | -------------------------------------------------------------------------------- /services/queue-mgr/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /services/queue-mgr/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.16.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.51" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.0.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 36 | 37 | [[package]] 38 | name = "backtrace" 39 | version = "0.3.61" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" 42 | dependencies = [ 43 | "addr2line", 44 | "cc", 45 | "cfg-if", 46 | "libc", 47 | "miniz_oxide", 48 | "object", 49 | "rustc-demangle", 50 | ] 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 57 | 58 | [[package]] 59 | name = "build_const" 60 | version = "0.2.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" 63 | 64 | [[package]] 65 | name = "byteorder" 66 | version = "0.5.3" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" 69 | 70 | [[package]] 71 | name = "byteorder" 72 | version = "1.4.3" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 75 | 76 | [[package]] 77 | name = "bytes" 78 | version = "1.1.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 81 | 82 | [[package]] 83 | name = "cc" 84 | version = "1.0.70" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" 87 | 88 | [[package]] 89 | name = "cfg-if" 90 | version = "1.0.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 93 | 94 | [[package]] 95 | name = "combine" 96 | version = "4.6.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" 99 | dependencies = [ 100 | "bytes", 101 | "memchr", 102 | ] 103 | 104 | [[package]] 105 | name = "crc" 106 | version = "1.8.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" 109 | dependencies = [ 110 | "build_const", 111 | ] 112 | 113 | [[package]] 114 | name = "dotenv" 115 | version = "0.15.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 118 | 119 | [[package]] 120 | name = "dtoa" 121 | version = "0.4.8" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" 124 | 125 | [[package]] 126 | name = "error-chain" 127 | version = "0.10.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" 130 | dependencies = [ 131 | "backtrace", 132 | ] 133 | 134 | [[package]] 135 | name = "flate2" 136 | version = "0.2.20" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "e6234dd4468ae5d1e2dbb06fe2b058696fdc50a339c68a393aefbf00bc81e423" 139 | dependencies = [ 140 | "libc", 141 | "miniz-sys", 142 | ] 143 | 144 | [[package]] 145 | name = "fnv" 146 | version = "1.0.7" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 149 | 150 | [[package]] 151 | name = "foreign-types" 152 | version = "0.3.2" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 155 | dependencies = [ 156 | "foreign-types-shared", 157 | ] 158 | 159 | [[package]] 160 | name = "foreign-types-shared" 161 | version = "0.1.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 164 | 165 | [[package]] 166 | name = "form_urlencoded" 167 | version = "1.0.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 170 | dependencies = [ 171 | "matches", 172 | "percent-encoding", 173 | ] 174 | 175 | [[package]] 176 | name = "getrandom" 177 | version = "0.2.3" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 180 | dependencies = [ 181 | "cfg-if", 182 | "libc", 183 | "wasi", 184 | ] 185 | 186 | [[package]] 187 | name = "gimli" 188 | version = "0.25.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" 191 | 192 | [[package]] 193 | name = "idna" 194 | version = "0.2.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 197 | dependencies = [ 198 | "matches", 199 | "unicode-bidi", 200 | "unicode-normalization", 201 | ] 202 | 203 | [[package]] 204 | name = "itoa" 205 | version = "0.4.8" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 208 | 209 | [[package]] 210 | name = "kafka" 211 | version = "0.8.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "f37f068eb07305e1141453ea2dccfb4f278153a4261bb9a519f10d1eb13d25a8" 214 | dependencies = [ 215 | "byteorder 0.5.3", 216 | "crc", 217 | "error-chain", 218 | "flate2", 219 | "fnv", 220 | "log 0.3.9", 221 | "openssl", 222 | "ref_slice", 223 | "snap", 224 | "twox-hash", 225 | ] 226 | 227 | [[package]] 228 | name = "lazy_static" 229 | version = "1.4.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 232 | 233 | [[package]] 234 | name = "libc" 235 | version = "0.2.101" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" 238 | 239 | [[package]] 240 | name = "log" 241 | version = "0.3.9" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 244 | dependencies = [ 245 | "log 0.4.14", 246 | ] 247 | 248 | [[package]] 249 | name = "log" 250 | version = "0.4.14" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 253 | dependencies = [ 254 | "cfg-if", 255 | ] 256 | 257 | [[package]] 258 | name = "matches" 259 | version = "0.1.9" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 262 | 263 | [[package]] 264 | name = "memchr" 265 | version = "2.4.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 268 | 269 | [[package]] 270 | name = "miniz-sys" 271 | version = "0.1.12" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "1e9e3ae51cea1576ceba0dde3d484d30e6e5b86dee0b2d412fe3a16a15c98202" 274 | dependencies = [ 275 | "cc", 276 | "libc", 277 | ] 278 | 279 | [[package]] 280 | name = "miniz_oxide" 281 | version = "0.4.4" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 284 | dependencies = [ 285 | "adler", 286 | "autocfg", 287 | ] 288 | 289 | [[package]] 290 | name = "object" 291 | version = "0.26.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2" 294 | dependencies = [ 295 | "memchr", 296 | ] 297 | 298 | [[package]] 299 | name = "once_cell" 300 | version = "1.8.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 303 | 304 | [[package]] 305 | name = "openssl" 306 | version = "0.10.36" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" 309 | dependencies = [ 310 | "bitflags", 311 | "cfg-if", 312 | "foreign-types", 313 | "libc", 314 | "once_cell", 315 | "openssl-sys", 316 | ] 317 | 318 | [[package]] 319 | name = "openssl-sys" 320 | version = "0.9.66" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" 323 | dependencies = [ 324 | "autocfg", 325 | "cc", 326 | "libc", 327 | "pkg-config", 328 | "vcpkg", 329 | ] 330 | 331 | [[package]] 332 | name = "percent-encoding" 333 | version = "2.1.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 336 | 337 | [[package]] 338 | name = "pkg-config" 339 | version = "0.3.19" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 342 | 343 | [[package]] 344 | name = "ppv-lite86" 345 | version = "0.2.10" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 348 | 349 | [[package]] 350 | name = "proc-macro2" 351 | version = "1.0.29" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" 354 | dependencies = [ 355 | "unicode-xid", 356 | ] 357 | 358 | [[package]] 359 | name = "queue-mgr" 360 | version = "0.1.0" 361 | dependencies = [ 362 | "dotenv", 363 | "kafka", 364 | "redis", 365 | ] 366 | 367 | [[package]] 368 | name = "quote" 369 | version = "1.0.9" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 372 | dependencies = [ 373 | "proc-macro2", 374 | ] 375 | 376 | [[package]] 377 | name = "rand" 378 | version = "0.8.4" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 381 | dependencies = [ 382 | "libc", 383 | "rand_chacha", 384 | "rand_core", 385 | "rand_hc", 386 | ] 387 | 388 | [[package]] 389 | name = "rand_chacha" 390 | version = "0.3.1" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 393 | dependencies = [ 394 | "ppv-lite86", 395 | "rand_core", 396 | ] 397 | 398 | [[package]] 399 | name = "rand_core" 400 | version = "0.6.3" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 403 | dependencies = [ 404 | "getrandom", 405 | ] 406 | 407 | [[package]] 408 | name = "rand_hc" 409 | version = "0.3.1" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 412 | dependencies = [ 413 | "rand_core", 414 | ] 415 | 416 | [[package]] 417 | name = "redis" 418 | version = "0.21.2" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "202c5bf92cad3d57605c366e644a7fbf305a83f19754fc66678c6265dcc9b8b4" 421 | dependencies = [ 422 | "async-trait", 423 | "combine", 424 | "dtoa", 425 | "itoa", 426 | "percent-encoding", 427 | "sha1", 428 | "url", 429 | ] 430 | 431 | [[package]] 432 | name = "ref_slice" 433 | version = "1.2.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "f4ed1d73fb92eba9b841ba2aef69533a060ccc0d3ec71c90aeda5996d4afb7a9" 436 | 437 | [[package]] 438 | name = "rustc-demangle" 439 | version = "0.1.21" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" 442 | 443 | [[package]] 444 | name = "sha1" 445 | version = "0.6.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" 448 | 449 | [[package]] 450 | name = "snap" 451 | version = "0.2.5" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "95d697d63d44ad8b78b8d235bf85b34022a78af292c8918527c5f0cffdde7f43" 454 | dependencies = [ 455 | "byteorder 1.4.3", 456 | "lazy_static", 457 | ] 458 | 459 | [[package]] 460 | name = "static_assertions" 461 | version = "1.1.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 464 | 465 | [[package]] 466 | name = "syn" 467 | version = "1.0.75" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" 470 | dependencies = [ 471 | "proc-macro2", 472 | "quote", 473 | "unicode-xid", 474 | ] 475 | 476 | [[package]] 477 | name = "tinyvec" 478 | version = "1.3.1" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" 481 | dependencies = [ 482 | "tinyvec_macros", 483 | ] 484 | 485 | [[package]] 486 | name = "tinyvec_macros" 487 | version = "0.1.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 490 | 491 | [[package]] 492 | name = "twox-hash" 493 | version = "1.6.1" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "1f559b464de2e2bdabcac6a210d12e9b5a5973c251e102c44c585c71d51bd78e" 496 | dependencies = [ 497 | "cfg-if", 498 | "rand", 499 | "static_assertions", 500 | ] 501 | 502 | [[package]] 503 | name = "unicode-bidi" 504 | version = "0.3.6" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" 507 | 508 | [[package]] 509 | name = "unicode-normalization" 510 | version = "0.1.19" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 513 | dependencies = [ 514 | "tinyvec", 515 | ] 516 | 517 | [[package]] 518 | name = "unicode-xid" 519 | version = "0.2.2" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 522 | 523 | [[package]] 524 | name = "url" 525 | version = "2.2.2" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 528 | dependencies = [ 529 | "form_urlencoded", 530 | "idna", 531 | "matches", 532 | "percent-encoding", 533 | ] 534 | 535 | [[package]] 536 | name = "vcpkg" 537 | version = "0.2.15" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 540 | 541 | [[package]] 542 | name = "wasi" 543 | version = "0.10.2+wasi-snapshot-preview1" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 546 | -------------------------------------------------------------------------------- /services/queue-mgr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "queue-mgr" 3 | version = "0.1.0" 4 | authors = ["Rafiusky "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | dotenv = "0.15.0" 9 | kafka = "0.8" 10 | redis = "*" 11 | -------------------------------------------------------------------------------- /services/queue-mgr/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate dotenv; 2 | 3 | use dotenv::dotenv; 4 | use kafka::client::{FetchOffset, GroupOffsetStorage}; 5 | use kafka::consumer::Consumer; 6 | use kafka::error::Error as KafkaError; 7 | use std::env; 8 | use std::string::String; 9 | use redis; 10 | 11 | fn redis_connect() -> redis::Connection { 12 | let redis_host = match env::var("REDIS_HOST") { 13 | Ok(val) => val, 14 | Err(_) => format!("{}", "localhost"), 15 | }; 16 | 17 | let redis_port = match env::var("REDIS_PORT") { 18 | Ok(val) => val, 19 | Err(_) => format!("{}", "localhost"), 20 | }; 21 | 22 | let redis_tls = match env::var("IS_TLS") { 23 | Ok(_) => "rediss", 24 | Err(_) => "redis", 25 | }; 26 | 27 | let redis_url = format!("{:}://{:}:{:}/", redis_tls, redis_host, redis_port); 28 | 29 | redis::Client::open(redis_url) 30 | .expect("invalid URL") 31 | .get_connection() 32 | .expect("unable to connect") 33 | } 34 | 35 | fn start_consumer(topic: String, brokers: Vec) -> Result<(), KafkaError> { 36 | let mut conn_redis = redis_connect(); 37 | 38 | let mut conn_kafka = Consumer::from_hosts(brokers) 39 | .with_topic(topic) 40 | .with_fallback_offset(FetchOffset::Earliest) 41 | .with_offset_storage(GroupOffsetStorage::Kafka) 42 | .create()?; 43 | 44 | loop { 45 | let mss = conn_kafka.poll()?; 46 | 47 | if mss.is_empty() { 48 | println!("No messages") 49 | } 50 | 51 | for ms in mss.iter(){ 52 | for m in ms.messages(){ 53 | 54 | let key = String::from_utf8_lossy(&m.key); 55 | let value = String::from_utf8_lossy(&m.value); 56 | 57 | let _: () = redis::cmd("SET") 58 | .arg(key.to_string()) 59 | .arg(value.to_string()) 60 | .query(&mut conn_redis) 61 | .expect("Not able to set key"); 62 | } 63 | let _ = conn_kafka.consume_messageset(ms); 64 | } 65 | conn_kafka.commit_consumed()?; 66 | } 67 | } 68 | 69 | fn main() { 70 | dotenv().ok(); 71 | 72 | 73 | let kafka_url = match env::var("KAFKA_URL") { 74 | Ok(val) => val, 75 | Err(_) => format!("{}", "localhost"), 76 | }; 77 | 78 | let kafka_port = match env::var("KAFKA_PORT") { 79 | Ok(val) => val, 80 | Err(_) => format!("{}", "localhost"), 81 | }; 82 | 83 | let kafka_topic = match env::var("KAFKA_TOPIC") { 84 | Ok(val) => val, 85 | Err(_) => format!("{}", "localhost"), 86 | }; 87 | 88 | let kafka_conn_url = format!("{:}:{:}", kafka_url, kafka_port); 89 | 90 | if let Err(e) = start_consumer(kafka_topic, vec![kafka_conn_url]) { 91 | println!("{:}", e) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | cd services/api 2 | go test ./... -v -cover 3 | --------------------------------------------------------------------------------