diff --git a/.drone.yml b/.drone.yml index 6758f2c..df3b1e5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,6 +4,7 @@ pipeline: group: build commands: - go get -v -d ./... + - sh build_plugins.sh - go build . test: image: golang @@ -12,7 +13,8 @@ pipeline: environment: [ test_api_token ] commands: - go get -v -d ./... - - go test ./... + - sh build_plugins.sh + - go test . ./commands ./settings ./plugin_manager/ ./shared/ publish: image: plugins/docker repo: klmp200/alfred diff --git a/.gitignore b/.gitignore index eb5b34a..ee19125 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,4 @@ settings_custom.json history.json -users.json -chat_data.json \ No newline at end of file +users.json \ No newline at end of file diff --git a/alfred.go b/alfred.go index b8d4956..1edaa8a 100644 --- a/alfred.go +++ b/alfred.go @@ -2,13 +2,14 @@ * @Author: Bartuccio Antoine * @Date: 2018-07-23 15:24:22 * @Last Modified by: klmp200 -* @Last Modified time: 2018-07-27 16:13:32 +* @Last Modified time: 2018-07-25 14:31:13 */ package main import ( "./commands" + "./plugin_manager" "./settings" "./shared" tb "gopkg.in/tucnak/telebot.v2" @@ -18,7 +19,7 @@ import ( func main() { registered_commands := map[string]func(*tb.Message){ - tb.OnText: commands.OnText, + // tb.OnText: commands.OnText, "/hello": commands.Hello, "/sponge": commands.Sponge, "/git": commands.Git, @@ -26,9 +27,6 @@ func main() { "/setgender": commands.SetGender, "/gender": commands.Gender, "/roll": commands.Dice, - "/trump": commands.LastTrumpTweet, - "/trends": commands.TwitterTrends, - "/chaos": commands.TwitterSJW, } if err := settings.LoadSettings("settings.json", "settings_custom.json"); err != nil { @@ -40,8 +38,6 @@ func main() { settings.Settings["history file"].(string)) log.Println("Initialize users infos") shared.InitUsers(settings.Settings["users file"].(string)) - log.Println("Initialize chat data") - shared.InitChatData(settings.Settings["chat data file"].(string)) log.Println("Bot initialisation") b, err := tb.NewBot(tb.Settings{ @@ -58,6 +54,9 @@ func main() { b.Handle(key, value) } + plugin_manager.Init("plugin", b) + b.Handle(tb.OnText, plugin_manager.HandleMessage) + plugin_manager.StartPlugins() log.Println("Starting bot") b.Start() } diff --git a/build_plugins.sh b/build_plugins.sh new file mode 100755 index 0000000..cfc47b8 --- /dev/null +++ b/build_plugins.sh @@ -0,0 +1,12 @@ +# @Author: Bartuccio Antoine +# @Date: 2018-07-25 12:47:23 +# @Last Modified by: klmp200 +# @Last Modified time: 2018-07-25 12:58:04 +#!/bin/sh +cd plugin +rm -f *.so +FILES=`ls *.go` +for FILE in $FILES +do + go build -buildmode=plugin $FILE +done \ No newline at end of file diff --git a/commands/gender.go b/commands/gender.go index 26d1d80..b126dbf 100644 --- a/commands/gender.go +++ b/commands/gender.go @@ -2,7 +2,7 @@ * @Author: Bartuccio Antoine * @Date: 2018-07-24 14:55:33 * @Last Modified by: klmp200 -* @Last Modified time: 2018-07-25 01:29:53 +* @Last Modified time: 2018-07-24 20:29:36 */ package commands @@ -24,10 +24,6 @@ func SetGender(m *tb.Message) { return } data := strings.Join(split, " ") - if data == "" { - shared.Bot.Send(m.Chat, "Attention, votre genre est vide. Ce n'est pas enregistrable.") - return - } shared.Users.Set(m.Sender.Username, "gender", data) shared.Bot.Send(m.Chat, "Votre genre est enregistré, je vous considère maintenant comme « "+data+" ».") } @@ -56,12 +52,13 @@ func Gender(m *tb.Message) { func cleanGender(slice []string) []string { for i := range slice { - slice[i] = strings.Replace(slice[i], "\\", "", -1) - slice[i] = strings.Replace(slice[i], "@", "", -1) clean := false for !clean { clean = true - if strings.HasPrefix(slice[i], "/") { + if strings.HasPrefix(slice[i], "@") { + slice[i] = strings.Replace(slice[i], "@", "", 1) + clean = false + } else if strings.HasPrefix(slice[i], "/") { slice[i] = strings.Replace(slice[i], "/", "", 1) clean = false } diff --git a/commands/twitter.go b/commands/twitter.go deleted file mode 100644 index 42e9ad8..0000000 --- a/commands/twitter.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -* @Author: Bartuccio Antoine -* @Date: 2018-07-25 18:51:38 -* @Last Modified by: klmp200 -* @Last Modified time: 2018-07-27 16:49:59 - */ - -package commands - -import ( - "../settings" - "../shared" - "github.com/dghubble/go-twitter/twitter" - "github.com/dghubble/oauth1" - tb "gopkg.in/tucnak/telebot.v2" - "strconv" - "strings" - "time" -) - -var client *twitter.Client - -func initTwitter() { - config := oauth1.NewConfig( - settings.Settings["twitter consumer key"].(string), - settings.Settings["twitter consumer secret"].(string), - ) - token := oauth1.NewToken( - settings.Settings["twitter access token"].(string), - settings.Settings["twitter access secret"].(string), - ) - - http_client := config.Client(oauth1.NoContext, token) - client = twitter.NewClient(http_client) -} - -func testOrInitTwitter() { - if client == nil { - initTwitter() - } -} - -func twitterCommunicationError(m *tb.Message) { - shared.Bot.Send(m.Chat, "Désolé, les serveurs de twitter sont injoignables.") -} - -func LastTrumpTweet(m *tb.Message) { - testOrInitTwitter() - user, _, err := client.Users.Show(&twitter.UserShowParams{ScreenName: "realDonaldTrump"}) - if err != nil { - twitterCommunicationError(m) - return - } - timeline, _, err := client.Timelines.UserTimeline(&twitter.UserTimelineParams{ScreenName: "realDonaldTrump"}) - if err != nil { - twitterCommunicationError(m) - return - } - response := []string{ - user.Name, - "\nFollowers : ", - strconv.Itoa(user.FollowersCount), - "\nStatus : ", - user.Description, - "\n ---", - "\n" + timeline[0].Text, - "\n ---", - "\n" + timeline[0].Source, - } - shared.Bot.Send(m.Chat, strings.Join(response, " ")) -} - -func TwitterTrends(m *tb.Message) { - testOrInitTwitter() - trends, _, err := client.Trends.Place(int64(615702), nil) - if err != nil { - twitterCommunicationError(m) - return - } - message := "Voici les dernières tendances en France" - for _, trend := range trends[0].Trends { - message += "\n" + trend.Name - } - shared.Bot.Send(m.Chat, message) -} - -func TwitterSJW(m *tb.Message) { - testOrInitTwitter() - last_use, exists := shared.ChatData.Get(m.Chat.ID, "last chaos use") - if exists { - var date time.Time - if _, is_string := last_use.(string); is_string { - date, _ = time.Parse(time.RFC3339, last_use.(string)) - } else { - date = last_use.(time.Time) - } - if time.Now().Before(date.Add(time.Hour * 24)) { - shared.Bot.Send(m.Chat, "Arioch ne répondra pas à votre appel.") - return - } - } - shared.ChatData.Set(m.Chat.ID, "last chaos use", time.Now()) - - tweets, _, err := client.Search.Tweets(&twitter.SearchTweetParams{ - Query: "#SJW", - }) - if err != nil { - twitterCommunicationError(m) - return - } - for _, tweet := range tweets.Statuses { - shared.Bot.Send(m.Chat, tweet.Text) - } -} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000..14c2637 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,68 @@ +package main + +import ( + "../plugin_manager" + tb "gopkg.in/tucnak/telebot.v2" +) + +type plugin string + +func (g plugin) GetCommands() []string { + return []string{"plugin"} +} + +func (g plugin) HandleCommand(bot *tb.Bot, msg *tb.Message, cmd string, args []string) { + if cmd == "plugin" { + ok := false + if len(args) >= 1 { + if args[0] == "list" { + lst := "" + for _, pName := range plugin_manager.GetPluginList() { + lst = lst + "-" + pName + " (status: " + if plugin_manager.IsPluginEnable(pName) { + lst += "enable, " + } else { + lst += "disable, " + } + if plugin_manager.IsPluginRunning(pName) { + lst += "running" + } else { + lst += "stopped" + } + lst += ")\n" + } + bot.Send(msg.Chat, "liste des plugins disponible:\n"+lst) + ok = true + } + if args[0] == "enable" && len(args) >= 2 { + for _, name := range args[1:] { + if plugin_manager.ExistPlugin(name) { + plugin_manager.EnablePlugin(name, true) + bot.Send(msg.Chat, "enable plugin "+name) + } + } + ok = true + } + if args[0] == "disable" && len(args) >= 2 { + for _, name := range args[1:] { + if plugin_manager.ExistPlugin(name) { + plugin_manager.EnablePlugin(name, false) + bot.Send(msg.Chat, "disable plugin "+name) + } + } + ok = true + } + } + if !ok { + bot.Send(msg.Chat, "command inconnue\n"+ + "liste de plugin:\n"+ + "/plugin list\n"+ + "activer un/des plugins\n"+ + "/plugin enable nom_plugin\n"+ + "désactiver un/des plugins\n"+ + "/plugin disable nom_plugin") + } + } +} + +var Plugin plugin diff --git a/plugin/test.go b/plugin/test.go new file mode 100644 index 0000000..6fc0446 --- /dev/null +++ b/plugin/test.go @@ -0,0 +1,40 @@ +package main + +import ( + tb "gopkg.in/tucnak/telebot.v2" + "log" +) + +type plugin string + +func (g plugin) GetCommands() []string { + return []string{"ping", "test"} +} + +func (g plugin) Load() bool { + log.Println("plugin test loaded!") + return true +} + +func (g plugin) HandleMessage(bot *tb.Bot, msg *tb.Message) { + log.Println("plugin test message: " + msg.Text) +} + +func (g plugin) HandleCommand(bot *tb.Bot, msg *tb.Message, cmd string, args []string) { + if cmd == "ping" { + bot.Send(msg.Chat, "pong!") + } + argsS := "" + for _, arg := range args { + argsS = argsS + " " + arg + } + log.Print("plugin test cmd: " + cmd + " (args:" + argsS + ")") + +} + +func (g plugin) Unload() bool { + log.Println("plugin test unloaded!") + return true +} + +var Plugin plugin diff --git a/plugin/test2.go b/plugin/test2.go new file mode 100644 index 0000000..9883f11 --- /dev/null +++ b/plugin/test2.go @@ -0,0 +1,22 @@ +package main + +import ( + tb "gopkg.in/tucnak/telebot.v2" + "log" +) + +type plugin string + +func (g plugin) Load() { + log.Println("plugin test2 loaded!") +} + +func (g plugin) HandleMessage(bot *tb.Bot, msg string) { + log.Println("test2 message: " + msg) +} + +func (g plugin) Unload() { + log.Println("plugin test2 unloaded!") +} + +var Plugin plugin diff --git a/plugin_manager/manager.go b/plugin_manager/manager.go new file mode 100644 index 0000000..c76dda8 --- /dev/null +++ b/plugin_manager/manager.go @@ -0,0 +1,162 @@ +/** + * @Author: KLIPFEL Arthur + * @Date: 2018-08-24 12:17:17 + */ +package plugin_manager + +import ( + tb "gopkg.in/tucnak/telebot.v2" + "log" + "path/filepath" + "strings" + "sync" +) + +type PluginCtrl struct { + plugin Plugin + mux sync.Mutex + running bool + enable bool +} + +var pluginDir string +var pluginsRunning bool +var plugins map[string]PluginCtrl +var context *tb.Bot + +func Init(_pluginDir string, bot *tb.Bot) { + pluginDir = _pluginDir + pluginsRunning = false + plugins = make(map[string]PluginCtrl) + context = bot + for _, fileName := range GetSoFiles(pluginDir) { + var p PluginCtrl + p.plugin = LoadSoFile(filepath.Join(pluginDir, fileName)) + if p.plugin != nil { + p.running = false + p.enable = true + plugins[fileName[:len(fileName)-3]] = p + } + } +} + +func GetPluginList() []string { + var lst []string + for name, _ := range plugins { + lst = append(lst, name) + } + return lst +} + +func IsPluginRunning(name string) bool { + if p, ok := plugins[name]; ok { + return p.running + } + return false +} + +func ExistPlugin(name string) bool { + if _, ok := plugins[name]; ok { + return true + } + return false +} + +func GetPlugin(name string) Plugin { + if p, ok := plugins[name]; ok { + return p + } + return nil +} + +func IsPluginEnable(name string) bool { + if p, ok := plugins[name]; ok { + return p.enable + } + return false +} + +func EnablePlugin(name string, enable bool) { + if p, ok := plugins[name]; ok { + if enable != p.enable { + p.enable = enable + plugins[name] = p + if pluginsRunning { + if enable { + if !p.running { + startPlugin(name) + } + } else { + if p.running { + stopPlugin(name) + } + } + } + } + } else { + log.Fatal("error: plugin " + name + " not founded") + } +} + +func StopPlugins() { + for k, _ := range plugins { + stopPlugin(k) + } + pluginsRunning = false +} + +func HandleMessage(msg *tb.Message) { + for _, val := range plugins { + if val.enable && val.running { + if strings.HasPrefix(msg.Text, "/") { + split := strings.Split(msg.Text, " ") + split[0] = split[0][1:] + if Contains(split[0], ExecGetCommands(val.plugin)) { + ExecHandleCommand(val.plugin, context, msg, split[0], split[1:]) + } + } else { + ExecHandleMessage(val.plugin, context, msg) + } + } + } +} + +func StartPlugins() { + for k, val := range plugins { + if val.enable { + startPlugin(k) + } + } + pluginsRunning = true +} + +func startPlugin(name string) { + if p, ok := plugins[name]; ok { + //p.mux.Lock() + if !p.running && p.enable { + p.running = ExecLoad(p.plugin) + plugins[name] = p + } + //p.mux.Unlock() + } +} + +func stopPlugin(name string) { + if p, ok := plugins[name]; ok { + //p.mux.Lock() + if p.running { + p.running = false + plugins[name] = p + ExecUnload(p.plugin) + } + //p.mux.Unlock() + } +} + +func Exit() { + for _, p := range plugins { + if p.running { + ExecUnload(p.plugin) + } + } +} diff --git a/plugin_manager/tools.go b/plugin_manager/tools.go new file mode 100644 index 0000000..3e15f72 --- /dev/null +++ b/plugin_manager/tools.go @@ -0,0 +1,127 @@ +/** + * @Author: KLIPFEL Arthur + * @Date: 2018-08-24 12:17:17 + */ +package plugin_manager + +import ( + tb "gopkg.in/tucnak/telebot.v2" + "io/ioutil" + "log" + "plugin" + "strings" +) + +type TestGetCommands interface { + GetCommands() []string +} + +type TestLoad interface { + Load() bool +} + +type TestHandleMessage interface { + HandleMessage(bot *tb.Bot, msg *tb.Message) +} + +type TestHandleCommand interface { + HandleCommand(bot *tb.Bot, msg *tb.Message, cmd string, args []string) +} + +type TestUnload interface { + Unload() bool +} + +type Plugin interface { /* + GetCommandes() []string + Load() bool + HandleMessage(bot *tb.Bot, msg *tb.Message) + HandleCommand(bot *tb.Bot, msg *tb.Message, cmd string, args []string) + Unload() bool*/ +} + +func GetSoFiles(dir string) []string { + var slice []string + + files, err := ioutil.ReadDir(dir) + if err != nil { + log.Fatal(err) + } else { + for _, f := range files { + if strings.HasSuffix(f.Name(), ".so") { + slice = append(slice, f.Name()) + } + } + } + return slice +} + +func LoadSoFile(file string) Plugin { + plug, err := plugin.Open(file) + if err != nil { + log.Fatal(err) + return nil + } + + symPlugin, err := plug.Lookup("Plugin") + if err != nil { + log.Fatal(err) + return nil + } + + var plugin Plugin + //plugin, _ = symPlugin.(Plugin) + plugin, ok := symPlugin.(Plugin) + if !ok { + log.Fatal(file + ": unexpected type from module symbol") + return nil + } + return plugin +} + +func ExecGetCommands(plugin Plugin) []string { + p, ok := plugin.(TestGetCommands) + if ok { + return p.GetCommands() + } + return []string{} +} + +func ExecLoad(plugin Plugin) bool { + p, ok := plugin.(TestLoad) + if ok { + return p.Load() + } + return true +} + +func ExecHandleMessage(plugin Plugin, bot *tb.Bot, msg *tb.Message) { + p, ok := plugin.(TestHandleMessage) + if ok { + p.HandleMessage(bot, msg) + } +} + +func ExecHandleCommand(plugin Plugin, bot *tb.Bot, msg *tb.Message, cmd string, args []string) { + p, ok := plugin.(TestHandleCommand) + if ok { + p.HandleCommand(bot, msg, cmd, args) + } +} + +func ExecUnload(plugin Plugin) bool { + p, ok := plugin.(TestUnload) + if ok { + return p.Unload() + } + return true +} + +func Contains(e string, s []string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/settings.json b/settings.json index de9860a..6c494ad 100644 --- a/settings.json +++ b/settings.json @@ -1,11 +1,6 @@ { "token": "INSERT TOKEN HERE", - "twitter access token": "INSERT TOKEN HERE", - "twitter access secret": "INSERT TOKEN HERE", - "twitter consumer key": "INSERT TOKEN HERE", - "twitter consumer secret": "INSERT TOKEN HERE", "history size": 10, "history file": "history.json", - "users file": "users.json", - "chat data file": "chat_data.json" -} \ No newline at end of file + "users file": "users.json" +} diff --git a/shared/chat.go b/shared/chat.go deleted file mode 100644 index 6158a28..0000000 --- a/shared/chat.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -* @Author: Bartuccio Antoine -* @Date: 2018-07-27 15:37:59 -* @Last Modified by: klmp200 -* @Last Modified time: 2018-07-27 16:06:51 - */ - -package shared - -import ( - "encoding/json" - "io/ioutil" - "sync" -) - -// General purpose chat info storage -type chatData struct { - mutex sync.Mutex - data map[int64]map[string]interface{} -} - -type chatDataFile struct { - mutex sync.Mutex - path string -} - -var ChatData chatData -var cdf chatDataFile - -// Init chat data meant to store infos about a chat. -func InitChatData(path string) { - cdf = chatDataFile{path: path} - ChatData = chatData{data: make(map[int64]map[string]interface{})} - ChatData.mutex.Lock() - defer ChatData.mutex.Unlock() - cdf.read() - -} - -func (c chatData) Set(chat int64, key string, data interface{}) { - c.mutex.Lock() - defer c.mutex.Unlock() - if _, exists := c.data[chat]; !exists { - c.data[chat] = make(map[string]interface{}) - } - c.data[chat][key] = data - go cdf.write() -} - -func (c chatData) Get(chat int64, key string) (interface{}, bool) { - c.mutex.Lock() - defer c.mutex.Unlock() - m, exists := c.data[chat] - if !exists { - return nil, false - } - data, ok := m[key] - if !ok { - return nil, false - } - return data, true -} - -func (c chatDataFile) read() { - c.mutex.Lock() - defer c.mutex.Unlock() - data, err := ioutil.ReadFile(c.path) - if err != nil { - // File doesn't exist, skip import - return - } - json.Unmarshal(data, &ChatData.data) -} - -func (c chatDataFile) write() { - c.mutex.Lock() - defer c.mutex.Unlock() - ChatData.mutex.Lock() - defer ChatData.mutex.Unlock() - data, _ := json.Marshal(ChatData.data) - ioutil.WriteFile(c.path, data, 0770) -}