Skip to content

Commit

Permalink
Merge pull request #1 from commonlispbr/feature/tests
Browse files Browse the repository at this point in the history
Add unit tests and CI config
  • Loading branch information
ryukinix committed Jul 24, 2020
2 parents 2c092c2 + c5056f0 commit 8b116f5
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 79 deletions.
62 changes: 62 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
on: [push, pull_request]
name: test
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: 1.13.x
- name: Checkout code
uses: actions/checkout@v1
- name: Install golangci-lint
run: |
go get github.com/golangci/golangci-lint/cmd/golangci-lint
- name: Run linters
run: |
export PATH=$PATH:$(go env GOPATH)/bin
golangci-lint -E bodyclose,misspell,gocyclo,dupl,gofmt,golint,unconvert,goimports,depguard,gocritic,funlen,interfacer run
test:
strategy:
matrix:
go-version: [1.13.x, 1.14.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
if: success()
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v1
- name: Run tests
run: go test -v -covermode=count

coverage:
runs-on: ubuntu-latest
steps:
- name: Install Go
if: success()
uses: actions/setup-go@v1
with:
go-version: 1.13.x
- name: Checkout code
uses: actions/checkout@v1
- name: Calc coverage
run: |
export PATH=$PATH:$(go env GOPATH)/bin
go test -v -covermode=count -coverprofile=coverage.out
- name: Convert coverage to lcov
uses: jandelgado/gcov2lcov-action@v1.0.0
with:
infile: coverage.out
outfile: coverage.lcov
- name: Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.lcov
flags: unittests
fail_ci_if_error: true
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
![test](https://github.com/commonlispbr/troll-shield/workflows/test/badge.svg)
![goreleaser](https://github.com/commonlispbr/troll-shield/workflows/goreleaser/badge.svg)
[![codecov](https://codecov.io/gh/commonlispbr/troll-shield/branch/master/graph/badge.svg?token=8YZDC9BGZX)](https://codecov.io/gh/commonlispbr/troll-shield)
# Usage

``` bash
Expand Down
188 changes: 109 additions & 79 deletions troll_shield.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ import (
"strings"
"sync"

tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
telegram "github.com/go-telegram-bot-api/telegram-bot-api"
)

// TrollShieldBot aggregate the methods used by my bot to keep mocking easier
type TrollShieldBot interface {
GetChatMember(telegram.ChatConfigWithUser) (telegram.ChatMember, error)
KickChatMember(telegram.KickChatMemberConfig) (telegram.APIResponse, error)
Send(telegram.Chattable) (telegram.Message, error)
}

// blacklist groups, member from that groups will be kicked automatically
var trollGroups = []string{
"@ccppbrasil",
Expand All @@ -31,16 +38,78 @@ const logfile = "troll-shield.log"

var log = logger.New(os.Stderr, "", logger.LstdFlags)

// findTrollHouse return the troll house group name if is well-known
// otherwise, returns a empty string
func findTrollHouses(bot *tgbotapi.BotAPI, userID int) string {
// messageEvent return true if is a message event
func messageEvent(update *telegram.Update) bool {
return update.Message != nil
}

// newChatMemberEvent return true if a new member joined to chat
func newChatMemberEvent(update *telegram.Update) bool {
return messageEvent(update) && update.Message.NewChatMembers != nil
}

// fromChatEvent return true if the message is from a specific chat
func fromChatEvent(update *telegram.Update, username string) bool {
chat := update.Message.Chat
return messageEvent(update) && chat != nil && (chat.UserName == username || chat.Title == username)
}

// getUserName return the most meaningful name available from a telegram user
// if it has a @username, return it
// if not, try to return FirstName + LastName
// otherwise, only return the FirstName
func getUserName(user telegram.User) string {
username := user.FirstName
if user.UserName != "" {
username = fmt.Sprintf("@%v", user.UserName)
} else if user.LastName != "" {
username = fmt.Sprintf("%s %s", user.FirstName, user.LastName)
}
return username
}

func getUpdates(bot *telegram.BotAPI) telegram.UpdatesChannel {
u := telegram.NewUpdate(0)
u.Timeout = 60
updates, err := bot.GetUpdatesChan(u)
if err != nil {
log.Printf("getUpdates error: %v", err)
}

return updates
}

func reply(bot TrollShieldBot, update *telegram.Update, text string) {
msg := telegram.NewMessage(update.Message.Chat.ID, text)
msg.ReplyToMessageID = update.Message.MessageID

_, err := bot.Send(msg)
if err != nil {
log.Printf("[!] Send msg failed: %v", err)
}
}

func welcomeMessage(bot TrollShieldBot, update *telegram.Update, member telegram.User) {
username := getUserName(member)
text := fmt.Sprintf(
`Olá %s! Seja bem-vindo ao grupo oficial de Common Lisp do Brasil.
Leia as regras em: https://lisp.com.br/rules.html.`,
username,
)
reply(bot, update, text)
}

// findTrollHouses return a string with groups separeted by comma,
// that groups are well-known to being troll houses.
// otherwise, if nothing is found returns a empty string
func findTrollHouses(bot TrollShieldBot, userID int) string {
ch := make(chan string, len(trollGroups))
var wait sync.WaitGroup
for _, trollGroup := range trollGroups {
wait.Add(1)
go func(group string) {
defer wait.Done()
c, _ := bot.GetChatMember(tgbotapi.ChatConfigWithUser{
c, _ := bot.GetChatMember(telegram.ChatConfigWithUser{
SuperGroupUsername: group,
UserID: userID,
})
Expand All @@ -66,20 +135,29 @@ func findTrollHouses(bot *tgbotapi.BotAPI, userID int) string {
return strings.Join(houses, ", ")
}

// messageEvent: return true if is a message event
func messageEvent(update *tgbotapi.Update) bool {
return update.Message != nil
}

// newChatMemberEvent: return true if a new member joined to chat
func newChatMemberEvent(update *tgbotapi.Update) bool {
return messageEvent(update) && update.Message.NewChatMembers != nil
}

// fromChatEvent: return true if the message is from a specific chat
func fromChatEvent(update *tgbotapi.Update, username string) bool {
chat := update.Message.Chat
return messageEvent(update) && chat != nil && (chat.UserName == username || chat.Title == username)
// kickTroll ban the troll and send a message about where we can found the trolls
func kickTroll(bot TrollShieldBot, update *telegram.Update, user telegram.User, trollHouse string) {
chatMember := telegram.ChatMemberConfig{
ChatID: update.Message.Chat.ID,
UserID: user.ID,
}
resp, err := bot.KickChatMember(
telegram.KickChatMemberConfig{ChatMemberConfig: chatMember},
)

if !resp.Ok || err != nil {
log.Printf(
"[!] Kicking %q did not work, error code %v: %v",
user.FirstName, resp.ErrorCode, resp.Description,
)
} else {
username := getUserName(user)
text := fmt.Sprintf(
"%v foi banido porque é membro do grupo: %v. Adeus.",
username, trollHouse,
)
reply(bot, update, text)
}
}

func setupLogging() {
Expand All @@ -92,39 +170,18 @@ func setupLogging() {

log.SetOutput(wrt)
// register log to BotLoggt
err = tgbotapi.SetLogger(log)
err = telegram.SetLogger(log)
if err != nil {
log.Printf("Set Telegram Bot Logging error: %v", err)
}
}

func getUserName(user tgbotapi.User) string {
username := user.FirstName
if user.UserName != "" {
username = fmt.Sprintf("@%v", user.UserName)
} else if user.LastName != "" {
username = fmt.Sprintf("%s %s", user.FirstName, user.LastName)
}
return username
}

func reply(bot *tgbotapi.BotAPI, update *tgbotapi.Update, text string) {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
msg.ReplyToMessageID = update.Message.MessageID

_, err := bot.Send(msg)
if err != nil {
log.Printf("[!] Send msg failed: %v", err)
}
}

func main() {
setupLogging()
func setupBot() *telegram.BotAPI {
token, exists := os.LookupEnv("TELEGRAM_BOT_TOKEN")
if !exists {
log.Fatal("TELEGRAM_BOT_TOKEN env should be defined.")
}
bot, err := tgbotapi.NewBotAPI(token)
bot, err := telegram.NewBotAPI(token)

if err != nil {
log.Panic(err)
Expand All @@ -134,12 +191,13 @@ func main() {

log.Printf("Authorized on account @%s", bot.Self.UserName)

u := tgbotapi.NewUpdate(0)
u.Timeout = 60

updates, err := bot.GetUpdatesChan(u)
return bot
}

for update := range updates {
func main() {
setupLogging()
bot := setupBot()
for update := range getUpdates(bot) {
if messageEvent(&update) {
if update.Message.Text == "/lelerax" {
reply(bot, &update, "Estou vivo.")
Expand All @@ -149,37 +207,9 @@ func main() {
if newChatMemberEvent(&update) {
for _, member := range *update.Message.NewChatMembers {
if trollHouse := findTrollHouses(bot, member.ID); trollHouse != "" {
chatMember := tgbotapi.ChatMemberConfig{
ChatID: update.Message.Chat.ID,
UserID: member.ID,
}
resp, err := bot.KickChatMember(
tgbotapi.KickChatMemberConfig{ChatMemberConfig: chatMember},
)

if resp.Ok == false || err != nil {
log.Printf(
"[!] Kicking %q did not work, error code %v: %v",
member.FirstName, resp.ErrorCode, resp.Description,
)
} else {
username := getUserName(member)
text := fmt.Sprintf(
"%v foi banido porque é membro do grupo: %v. Adeus.",
username, trollHouse,
)
reply(bot, &update, text)
}
} else {
if fromChatEvent(&update, "commonlispbr") && !member.IsBot {
username := getUserName(member)
text := fmt.Sprintf(
`Olá %s! Seja bem-vindo ao grupo oficial de Common Lisp do Brasil.
Leia as regras em: https://lisp.com.br/rules.html.`,
username,
)
reply(bot, &update, text)
}
kickTroll(bot, &update, member, trollHouse)
} else if fromChatEvent(&update, "commonlispbr") && !member.IsBot {
welcomeMessage(bot, &update, member)
}
}
}
Expand Down
Loading

0 comments on commit 8b116f5

Please sign in to comment.