├── Dockerfile ├── README.md ├── app.py ├── authmsg ├── bot.py ├── offset ├── requirements.txt └── token /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.2-alpine 2 | 3 | ENV AUTHMSG "start" 4 | ENV TOKEN "NNN:XXX" 5 | 6 | COPY ./* /work/ 7 | WORKDIR /work 8 | 9 | RUN pip install flask && \ 10 | pip install requests &&\ 11 | pip install ipdb 12 | 13 | EXPOSE 10111 14 | CMD echo ${AUTHMSG} > /work/authmsg && echo ${TOKEN} > /work/token && python app.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitlab webhook telegram bot 2 | 3 | Simple gitlab telegram bot that listen to gitlab webhooks and send each event 4 | to the authenticated chats 5 | 6 | https://core.telegram.org/bots 7 | 8 | Create a new bot https://core.telegram.org/bots#create-a-new-bot 9 | and then copy the token to the token file. 10 | 11 | # Requirements 12 | Only work with python3 13 | 14 | # How to use 15 | 16 | 1. Change the authmsg file with some secret keyworld 17 | 1. Run the app.py in your server 18 | 1. Create a webhook in your gitlab project that points to 19 | http://yourserver:10111/ 20 | 1. Talk to your bot and write only the keyworld 21 | 1. You will receive each event in your repo 22 | 23 | # FAQ 24 | 25 | ## Q. How can I stop receiving messages 26 | R. Write "shutupbot" in your conversation and the bot won't talk to you anymore 27 | 28 | ## Q. How can I enable the bot in group chats 29 | R. Write /keyworld instead of keyworld 30 | 31 | # Interesting files 32 | 33 | * chats, the json with all the chats to send notifications 34 | * token, the bot token 35 | * offset, the last msg id received from telegram api 36 | 37 | # Docker 38 | ## build 39 | ```shell 40 | $ docker build -t bot . 41 | ``` 42 | ## run 43 | ```shell 44 | $ docker run -d -p 10111:10111 --name bot -e AUTHMSG="XXX" -e TOKEN="XXX:XXX" bot 45 | ``` -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | from flask import Flask 5 | from flask import request 6 | from flask import jsonify 7 | from bot import Bot 8 | app = Flask(__name__) 9 | 10 | class GitlabBot(Bot): 11 | def __init__(self): 12 | try: 13 | self.authmsg = open('authmsg').read().strip() 14 | except: 15 | raise Exception("The authorization messsage file is invalid") 16 | 17 | super(GitlabBot, self).__init__() 18 | self.chats = {} 19 | try: 20 | chats = open('chats', 'r').read() 21 | self.chats = json.loads(chats) 22 | except: 23 | open('chats', 'w').write(json.dumps(self.chats)) 24 | 25 | self.send_to_all('Hi !') 26 | 27 | def text_recv(self, txt, chatid): 28 | ''' registering chats ''' 29 | txt = txt.strip() 30 | if txt.startswith('/'): 31 | txt = txt[1:] 32 | if txt == self.authmsg: 33 | if str(chatid) in self.chats: 34 | self.reply(chatid, "\U0001F60E boy, you already got the power.") 35 | else: 36 | self.reply(chatid, "\U0001F60E Ok boy, you got the power !") 37 | self.chats[chatid] = True 38 | open('chats', 'w').write(json.dumps(self.chats)) 39 | elif txt == 'shutupbot': 40 | del self.chats[chatid] 41 | self.reply(chatid, "\U0001F63F Ok, take it easy\nbye.") 42 | open('chats', 'w').write(json.dumps(self.chats)) 43 | else: 44 | self.reply(chatid, "\U0001F612 I won't talk to you.") 45 | 46 | def send_to_all(self, msg): 47 | for c in self.chats: 48 | self.reply(c, msg) 49 | 50 | 51 | b = GitlabBot() 52 | 53 | 54 | @app.route("/", methods=['GET', 'POST']) 55 | def webhook(): 56 | data = request.json 57 | # json contains an attribute that differenciates between the types, see 58 | # https://docs.gitlab.com/ce/user/project/integrations/webhooks.html 59 | # for more infos 60 | kind = data['object_kind'] 61 | if kind == 'push': 62 | msg = generatePushMsg(data) 63 | elif kind == 'tag_push': 64 | msg = generatePushMsg(data) # TODO:Make own function for this 65 | elif kind == 'issue': 66 | msg = generateIssueMsg(data) 67 | elif kind == 'note': 68 | msg = generateCommentMsg(data) 69 | elif kind == 'merge_request': 70 | msg = generateMergeRequestMsg(data) 71 | elif kind == 'wiki_page': 72 | msg = generateWikiMsg(data) 73 | elif kind == 'pipeline': 74 | msg = generatePipelineMsg(data) 75 | elif kind == 'build': 76 | msg = generateBuildMsg(data) 77 | b.send_to_all(msg) 78 | return jsonify({'status': 'ok'}) 79 | 80 | 81 | def generatePushMsg(data): 82 | msg = '*{0} ({1}) - {2} new commits*\n'\ 83 | .format(data['project']['name'], data['project']['default_branch'], data['total_commits_count']) 84 | for commit in data['commits']: 85 | msg = msg + '----------------------------------------------------------------\n' 86 | msg = msg + commit['message'].rstrip() 87 | msg = msg + '\n' + commit['url'].replace("_", "\_") + '\n' 88 | msg = msg + '----------------------------------------------------------------\n' 89 | return msg 90 | 91 | 92 | def generateIssueMsg(data): 93 | action = data['object_attributes']['action'] 94 | if action == 'open': 95 | assignees = '' 96 | for assignee in data.get('assignees', []): 97 | assignees += assignee['name'] + ' ' 98 | msg = '*{0}* new issue for *{1}*:\n'\ 99 | .format(data['project']['name'], assignees) 100 | elif action == 'reopen': 101 | assignees = '' 102 | for assignee in data.get('assignees', []): 103 | assignees += assignee['name'] + ' ' 104 | msg = '*{0}* issue re-opened for *{1}*:\n'\ 105 | .format(data['project']['name'], assignees) 106 | elif action == 'close': 107 | msg = '*{0}* issue closed by *{1}*:\n'\ 108 | .format(data['project']['name'], data['user']['name']) 109 | elif action == 'update': 110 | assignees = '' 111 | for assignee in data.get('assignees', []): 112 | assignees += assignee['name'] + ' ' 113 | msg = '*{0}* issue assigned to *{1}*:\n'\ 114 | .format(data['project']['name'], assignees) 115 | 116 | msg = msg + '[{0}]({1})'\ 117 | .format(data['object_attributes']['title'], data['object_attributes']['url']) 118 | return msg 119 | 120 | 121 | def generateCommentMsg(data): 122 | ntype = data['object_attributes']['noteable_type'] 123 | if ntype == 'Commit': 124 | msg = 'note to commit' 125 | elif ntype == 'MergeRequest': 126 | msg = 'note to MergeRequest' 127 | elif ntype == 'Issue': 128 | msg = 'note to Issue' 129 | elif ntype == 'Snippet': 130 | msg = 'note on code snippet' 131 | return msg 132 | 133 | 134 | def generateMergeRequestMsg(data): 135 | return 'new MergeRequest' 136 | 137 | 138 | def generateWikiMsg(data): 139 | return 'new wiki stuff' 140 | 141 | 142 | def generatePipelineMsg(data): 143 | return 'new pipeline stuff' 144 | 145 | 146 | def generateBuildMsg(data): 147 | return 'new build stuff' 148 | 149 | 150 | if __name__ == "__main__": 151 | b.run_threaded() 152 | app.run(host='0.0.0.0', port=10111) 153 | -------------------------------------------------------------------------------- /authmsg: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXXXXXXXXXXXXXX -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | import requests 5 | from threading import Thread 6 | 7 | 8 | class Bot: 9 | def __init__(self): 10 | try: 11 | self.token = open('token').read().split()[0] 12 | except: 13 | raise Exception("The token file is invalid") 14 | 15 | self.api = 'https://api.telegram.org/bot%s/' % self.token 16 | try: 17 | self.offset = int(open('offset').read().split()[0]) 18 | except: 19 | self.offset = 0 20 | self.me = self.botq('getMe') 21 | self.running = False 22 | 23 | def botq(self, method, params=None): 24 | url = self.api + method 25 | params = params if params else {} 26 | return requests.post(url, params).json() 27 | 28 | def msg_recv(self, msg): 29 | ''' method to override ''' 30 | pass 31 | 32 | def text_recv(self, txt, chatid): 33 | ''' method to override ''' 34 | pass 35 | 36 | def updates(self): 37 | data = {'offset': self.offset} 38 | r = self.botq('getUpdates', data) 39 | for up in r['result']: 40 | if 'message' in up: 41 | self.msg_recv(up['message']) 42 | elif 'edited_message' in up: 43 | self.msg_recv(up['edited_message']) 44 | else: 45 | # not a valid message 46 | break 47 | 48 | try: 49 | txt = up['message']['text'] 50 | self.text_recv(txt, self.get_to_from_msg(up['message'])) 51 | except: 52 | pass 53 | self.offset = up['update_id'] 54 | self.offset += 1 55 | open('offset', 'w').write('%s' % self.offset) 56 | 57 | def get_to_from_msg(self, msg): 58 | to = '' 59 | try: 60 | to = msg['chat']['id'] 61 | except: 62 | to = '' 63 | return to 64 | 65 | def reply(self, to, msg): 66 | if type(to) not in [int, str]: 67 | to = self.get_to_from_msg(to) 68 | resp = self.botq('sendMessage', {'chat_id': to, 'text': msg, 'disable_web_page_preview': True, 'parse_mode': 'Markdown'}) 69 | return resp 70 | 71 | def run(self): 72 | self.running = True 73 | while self.running: 74 | self.updates() 75 | time.sleep(1) 76 | 77 | def run_threaded(self): 78 | t = Thread(target=self.run) 79 | t.start() 80 | 81 | def stop(self): 82 | self.running = False 83 | 84 | 85 | if __name__ == '__main__': 86 | bot = Bot() 87 | bot.run() 88 | -------------------------------------------------------------------------------- /offset: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | requests 3 | ipdb 4 | -------------------------------------------------------------------------------- /token: -------------------------------------------------------------------------------- 1 | NNNNNN:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 2 | --------------------------------------------------------------------------------