├── .gitignore ├── .rsyncignore ├── LICENSE ├── README.mdown ├── data └── harbour-slackfish.png ├── harbour-slackfish.desktop ├── main.go ├── makefile ├── qml ├── cover │ └── CoverPage.qml ├── harbour-slackfish.qml ├── images │ └── slack_rgb.png ├── js │ ├── logic │ │ ├── auth.js │ │ └── users.js │ └── services │ │ └── slackWorker.js └── pages │ ├── About.qml │ ├── AuthPage.qml │ ├── ChannelListPage.qml │ ├── ChannelPage.qml │ ├── Image.qml │ └── Profile.qml ├── rpm ├── harbour-slackfish.changes.in └── harbour-slackfish.spec ├── settings └── settings.go ├── slack ├── channels.go ├── imChannels.go ├── logger.go ├── messages.go ├── slack.go └── users.go └── translations ├── harbour-slackfish-de.ts └── harbour-slackfish.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *-build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | #QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | #QTCreator build 37 | build-* 38 | -------------------------------------------------------------------------------- /.rsyncignore: -------------------------------------------------------------------------------- 1 | - .* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robert Scheinpflug 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 | 23 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | Slackfish - Slack client on Jolla Sailfish OS 2 | ----------- 3 | 4 | Slack client on Jolla Sailfish OS 5 | 6 | *Note: This project is not the source for the `"Slackfish"` application currently found in Jolla Store. That application is found [here](https://github.com/markussammallahti/harbour-slackfish).* 7 | 8 | ### Project status 9 | 10 | **STATUS UPDATE: Development stopped since I am not using SailfishOS anymore (for now).** 11 | 12 | _ | date 13 | :------------- | :------------- 14 | alpha | see [milestone alpha](https://github.com/neversun/Slackfish/milestone/1) 15 | beta | see [milestone beta](https://github.com/neversun/Slackfish/milestone/2) 16 | 1.0 | see [milestone 1.0](https://github.com/neversun/Slackfish/milestone/3) 17 | Release to Jolla Store | TBD 18 | 19 | ## Build on MerSDK 20 | 21 | - Install https://github.com/nekrondev/jolla_go on MerSDK 22 | - Run `make` 23 | 24 | ## Install go dependencies on development machine 25 | 26 | ``` 27 | sudo apt-get install libqt5quick5 qtdeclarative5-dev qt5-qmake \ 28 | libglib2.0-dev qt5-default libffi-dev libsqlite3-dev \ 29 | qtbase5-private-dev qtdeclarative5-private-dev \ 30 | libqt5opengl5-dev qtdeclarative5-qtquick2-plugin 31 | 32 | go get ./... 33 | ``` 34 | -------------------------------------------------------------------------------- /data/harbour-slackfish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neversun/Slackfish/22766d130fb2bd24f152dbe8d1eb78ca2610f2cd/data/harbour-slackfish.png -------------------------------------------------------------------------------- /harbour-slackfish.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | X-Nemo-Application-Type=generic 4 | Icon=harbour-slackfish 5 | Exec=harbour-slackfish 6 | Name=Slackfish 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/neversun/Slackfish/settings" 11 | slack "github.com/neversun/Slackfish/slack" 12 | qml "gopkg.in/qml.v1" 13 | ) 14 | 15 | const ( 16 | // Appname is the package name 17 | Appname = "harbour-slackfish" 18 | ) 19 | 20 | // SlackfishControl is exported to QML and enables interaction with API 21 | type SlackfishControl struct { 22 | Root qml.Object 23 | Slack *slack.Model 24 | ChannelsModel *slack.Channels 25 | SettingsModel *settings.Settings 26 | MessagesModel *slack.Messages 27 | UsersModel *slack.Users 28 | ImChannelsModel *slack.IMs 29 | } 30 | 31 | func main() { 32 | if err := qml.SailfishRun(run); err != nil { 33 | fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | func run() error { 39 | ss := &settings.Settings{} 40 | 41 | slackfish := SlackfishControl{ 42 | Slack: &slack.Slack, 43 | ChannelsModel: &slack.Slack.Channels, 44 | SettingsModel: ss, 45 | MessagesModel: &slack.Slack.Messages, 46 | UsersModel: &slack.Slack.Users, 47 | ImChannelsModel: &slack.Slack.IMs, 48 | } 49 | 50 | engine := qml.SailfishNewEngine() 51 | 52 | path, err := getPath() 53 | if err != nil { 54 | panic(err) 55 | } 56 | dataDir := filepath.Join(path, ".local", "share", Appname) 57 | set := settings.Settings{Location: filepath.Join(dataDir, "settings.yml")} 58 | slackfish.SettingsModel = &set 59 | 60 | if err = slackfish.SettingsModel.Load(); err != nil { 61 | log.Printf("WARN: %+v\n", err) 62 | } 63 | 64 | // TODO: implement translation 65 | // engine.Translator("/usr/share/harbour-slackfish/qml/i18n") 66 | 67 | context := engine.Context() 68 | context.SetVar("slackfishctrl", &slackfish) 69 | // publish direct bindings to public members of slackfish 70 | context.SetVars(&slackfish) 71 | 72 | controls, err := engine.SailfishSetSource("qml/" + Appname + ".qml") 73 | if err != nil { 74 | return err 75 | } 76 | 77 | window := controls.SailfishCreateWindow() 78 | slackfish.Root = window.Root() 79 | 80 | window.SailfishShow() 81 | window.Wait() 82 | 83 | return nil 84 | } 85 | 86 | func getPath() (string, error) { 87 | path := os.Getenv("XDG_DATA_HOME") 88 | if len(path) == 0 { 89 | path = os.Getenv("HOME") 90 | if len(path) == 0 { 91 | return "", errors.New("No XDG_DATA or HOME env set") 92 | } 93 | } 94 | return path, nil 95 | } 96 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | SSH_SAILFISH = ssh -p 2222 -i $(HOME)/SailfishOS/vmshare/ssh/private_keys/engine/mersdk mersdk@localhost $(1) 2 | 3 | build: setup sync sailfish_build deploy 4 | 5 | deploy: sailfish_deploy 6 | 7 | setup: 8 | $(call SSH_SAILFISH,'mkdir -p /home/mersdk/src/github.com/neversun/Slackfish/') 9 | 10 | sync: 11 | rsync --exclude-from=.rsyncignore -Eahrve 'ssh -p 2222 -i $(HOME)/SailfishOS/vmshare/ssh/private_keys/engine/mersdk' $(HOME)/go/src/github.com/neversun/Slackfish mersdk@localhost:/home/mersdk/src/github.com/neversun 12 | 13 | clean: 14 | $(call SSH_SAILFISH,'rm -rf /home/mersdk/src/github.com/neversun/Slackfish/') 15 | 16 | sailfish_build: 17 | $(call SSH_SAILFISH,'cd /home/mersdk/src/github.com/neversun/Slackfish/; mb2 build') 18 | 19 | sailfish_deploy: 20 | $(call SSH_SAILFISH,'cd /home/mersdk/src/github.com/neversun/Slackfish/; mb2 -s rpm/harbour-slackfish.spec -d "SailfishOS Emulator" deploy --sdk') 21 | -------------------------------------------------------------------------------- /qml/cover/CoverPage.qml: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2013 Jolla Ltd. 3 | Contact: Thomas Perl 4 | All rights reserved. 5 | 6 | You may use this file under the terms of BSD license as follows: 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the Jolla Ltd nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | import QtQuick 2.0 32 | import Sailfish.Silica 1.0 33 | 34 | CoverBackground { 35 | Label { 36 | id: label 37 | anchors.centerIn: parent 38 | text: qsTr("My Cover") 39 | } 40 | 41 | CoverActionList { 42 | id: coverAction 43 | 44 | CoverAction { 45 | iconSource: "image://theme/icon-cover-next" 46 | } 47 | 48 | CoverAction { 49 | iconSource: "image://theme/icon-cover-pause" 50 | } 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /qml/harbour-slackfish.qml: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2013 Jolla Ltd. 3 | Contact: Thomas Perl 4 | All rights reserved. 5 | 6 | You may use this file under the terms of BSD license as follows: 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the Jolla Ltd nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | import QtQuick 2.0 32 | import Sailfish.Silica 1.0 33 | import "pages" 34 | import "js/logic/auth.js" as AuthLogic 35 | 36 | ApplicationWindow 37 | { 38 | cover: Qt.resolvedUrl("cover/CoverPage.qml") 39 | allowedOrientations: Orientation.All 40 | 41 | Component.onCompleted: { 42 | if (settingsModel.token !== '') { 43 | AuthLogic.tokenReceived(settingsModel.token) 44 | } else { 45 | pageStack.push(Qt.resolvedUrl("pages/AuthPage.qml")) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /qml/images/slack_rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neversun/Slackfish/22766d130fb2bd24f152dbe8d1eb78ca2610f2cd/qml/images/slack_rgb.png -------------------------------------------------------------------------------- /qml/js/logic/auth.js: -------------------------------------------------------------------------------- 1 | function workerOnMessage (messageObject) { 2 | if (messageObject.apiMethod === 'oauth.access') { 3 | tokenReceived(messageObject.data.access_token) 4 | } 5 | } 6 | 7 | function webViewUrlChanged (url) { 8 | url = url.toString() 9 | if (url.indexOf('redirect_uri') !== -1) { 10 | return 11 | } 12 | 13 | var codeIndex = url.indexOf('code=') 14 | if (codeIndex === -1) { 15 | return 16 | } 17 | 18 | authentificated(url.substring(codeIndex + 5).replace('&state=', '')) 19 | } 20 | 21 | function authentificated (code) { 22 | slackWorker.sendMessage({'apiMethod': 'oauth.access', 'code': code}) 23 | } 24 | 25 | function tokenReceived (token) { 26 | // TODO: check if token is valid right now 27 | slack.connect(token) 28 | 29 | pageStack.replace(Qt.resolvedUrl('../../pages/ChannelListPage.qml')) 30 | 31 | settingsModel.token = token 32 | settingsModel.save() 33 | } 34 | -------------------------------------------------------------------------------- /qml/js/logic/users.js: -------------------------------------------------------------------------------- 1 | function get (id) { 2 | var rawId 3 | if (id) { 4 | rawId = Array.isArray(id) ? id[0] : id 5 | } 6 | rawId = rawId || '' 7 | 8 | var response = JSON.parse(usersModel.get(rawId)) 9 | return rawId ? response[rawId] : response 10 | } 11 | 12 | function handleLink (link) { 13 | if (/^slackfish:\/\//.test(link)) { 14 | var split = /^slackfish:\/\/(.*?)\/(.*)/.exec(link) 15 | pageStack.push(Qt.resolvedUrl('../../pages/' + split[1] + '.qml'), JSON.parse(decodeURIComponent(split[2]))) 16 | return 17 | } 18 | 19 | console.log('external link', link) 20 | Qt.openUrlExternally(link) 21 | } 22 | -------------------------------------------------------------------------------- /qml/js/services/slackWorker.js: -------------------------------------------------------------------------------- 1 | /* attributes */ 2 | var baseUrl = 'https://slack.com/api/' 3 | var clientID = '14600308501.14604351382' 4 | var clientSecret = 'bf0b801b1b4716b72554f4415c6df6fd' 5 | var redirectUri = 'http%3A%2F%2F0.0.0.0%3A12345%2Foauth' 6 | 7 | /* Distributes calls of Slack API functions */ 8 | WorkerScript.onMessage = function (data) { 9 | console.log('workerScript onMessage:', JSON.stringify(data)) 10 | if (data.apiMethod === 'oauth.access') { 11 | oauthAccessRequest(data.apiMethod, data.code) 12 | } else { 13 | console.log('Unknown request to workerScript') 14 | } 15 | } 16 | 17 | function oauthAccessRequest (apiMethod, code) { 18 | var endpoint = baseUrl + apiMethod + 19 | '?client_id=' + clientID + 20 | '&client_secret=' + clientSecret + 21 | '&code=' + code + 22 | '&redirect_uri=' + redirectUri 23 | httpGet(endpoint, apiMethod) 24 | } 25 | 26 | function httpGet (endpoint, apiMethod) { 27 | var http = new XMLHttpRequest() 28 | 29 | http.onreadystatechange = function () { 30 | if (http.readyState === XMLHttpRequest.DONE) { 31 | if(http.status === 200) { 32 | WorkerScript.sendMessage({ 'status': 'done', 'data': JSON.parse(http.responseText), 'apiMethod': apiMethod }) 33 | } else { 34 | WorkerScript.sendMessage({ 'status': 'error', 'data': null, 'apiMethod': apiMethod }) 35 | } 36 | } 37 | } 38 | 39 | http.open('GET', endpoint) 40 | http.send() 41 | } 42 | -------------------------------------------------------------------------------- /qml/pages/About.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page{ 5 | id: aboutPage 6 | allowedOrientations: Orientation.All 7 | 8 | SilicaFlickable { 9 | id: flickerList 10 | anchors.fill: aboutPage 11 | contentHeight: content.height 12 | 13 | Column { 14 | id: content 15 | anchors { 16 | left: parent.left 17 | right: parent.right 18 | margins: Theme.paddingLarge 19 | } 20 | spacing: Theme.paddingMedium 21 | 22 | PageHeader { 23 | title: "About" 24 | width: parent.width 25 | } 26 | 27 | Column { 28 | id: portrait 29 | width: parent.width 30 | 31 | SectionHeader { 32 | text: 'Made by' 33 | } 34 | 35 | Label { 36 | text: 'neversun' 37 | anchors.horizontalCenter: parent.horizontalCenter 38 | } 39 | 40 | SectionHeader { 41 | text: 'Source' 42 | } 43 | 44 | Label { 45 | text: "github.com" 46 | font.underline: true; 47 | anchors.horizontalCenter: parent.horizontalCenter 48 | MouseArea { 49 | anchors.fill : parent 50 | onClicked: Qt.openUrlExternally("https://github.com/neversun/Slackfish") 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /qml/pages/AuthPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../js/logic/auth.js" as Logic 4 | 5 | Page { 6 | id: authPage 7 | property string client_id: "14600308501.14604351382" 8 | property string scope: "client" 9 | property string redirect_uri : "http://0.0.0.0:12345/oauth" 10 | property string auth_url : "https://slack.com/oauth/authorize?client_id=" + client_id + "&scope=" + scope + "&redirect_uri=" + redirect_uri 11 | property bool showWebview : false 12 | 13 | WorkerScript { 14 | id: slackWorker 15 | source: "../js/services/slackWorker.js" 16 | onMessage: { 17 | Logic.workerOnMessage(messageObject); 18 | } 19 | } 20 | 21 | Column { 22 | id: col 23 | spacing: 15 24 | visible: !showWebview 25 | anchors.fill: parent 26 | PageHeader { 27 | title: "Slackfish" 28 | } 29 | Image { 30 | width: parent.width 31 | height: parent.height/4 32 | anchors.horizontalCenter: parent.horizontalCenter 33 | source: "../images/slack_rgb.png" 34 | } 35 | 36 | Label { 37 | text: "Welcome to Slackfish, an unoffical Slack client for Sailfish OS.
Please press 'continue' to login or create a StackExchange account.

This app is not created by, affiliated with, or supported by Slack Technologies, Inc." 38 | anchors.left: parent.left 39 | anchors.leftMargin: Theme.paddingLarge 40 | anchors.right: parent.right 41 | anchors.rightMargin: Theme.paddingLarge 42 | wrapMode: Text.Wrap 43 | textFormat: Text.RichText 44 | color: Theme.highlightColor 45 | } 46 | Button { 47 | anchors.horizontalCenter: parent.horizontalCenter 48 | text: "Continue" 49 | onClicked : { 50 | webview.url = auth_url; 51 | webview.visible = true; 52 | showWebview = true; 53 | } 54 | } 55 | } 56 | 57 | SilicaWebView { 58 | id: webview 59 | visible: showWebview 60 | anchors.fill: parent 61 | onUrlChanged: { 62 | Logic.webViewUrlChanged(url); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /qml/pages/ChannelListPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../js/logic/users.js" as UsersLogic 4 | 5 | Page { 6 | // init 7 | id: channelListPage 8 | allowedOrientations: Orientation.All 9 | 10 | // view 11 | 12 | SilicaFlickable { 13 | anchors.fill: parent 14 | contentHeight: content.height 15 | 16 | PullDownMenu { 17 | MenuItem { 18 | text: "About" 19 | onClicked: pageStack.push(Qt.resolvedUrl("About.qml")) 20 | } 21 | MenuItem { 22 | text: "Sign out" 23 | onClicked: { 24 | settingsModel.token = '' 25 | settingsModel.save() 26 | slack.disconnect() 27 | pageStack.replace(Qt.resolvedUrl("AuthPage.qml")) 28 | } 29 | } 30 | } 31 | 32 | Column { 33 | id: content 34 | width: parent.width 35 | 36 | PageHeader { 37 | title: qsTr("Channels") 38 | } 39 | 40 | ColumnView { 41 | model: channelsModel.len 42 | itemHeight: Theme.itemSizeSmall 43 | 44 | delegate: ListItem { 45 | onClicked: { 46 | pageStack.push(Qt.resolvedUrl("ChannelPage.qml"), { channelIndex: index, type: 'channel'}) 47 | } 48 | 49 | Label { 50 | text: '#' + channelsModel.get(index).name 51 | font.pixelSize: Theme.fontSizeLarge 52 | width: parent.width 53 | color: highlighted ? Theme.highlightColor : Theme.primaryColor 54 | horizontalAlignment: Text.AlignHCenter 55 | } 56 | } 57 | } 58 | 59 | PageHeader { 60 | title: qsTr("Direct messages") 61 | } 62 | 63 | ColumnView { 64 | model: imChannelsModel.len 65 | itemHeight: Theme.itemSizeSmall 66 | 67 | delegate: ListItem { 68 | property variant user: UsersLogic.get(imChannelsModel.get(index).user) 69 | onClicked: { 70 | imChannelsModel.open(user.id) 71 | pageStack.push(Qt.resolvedUrl("ChannelPage.qml"), { channelIndex: index, type: 'im'}) 72 | } 73 | 74 | Label { 75 | text: { 76 | return user.realName || user.name 77 | } 78 | font.pixelSize: Theme.fontSizeLarge 79 | width: parent.width 80 | color: highlighted ? Theme.highlightColor : Theme.primaryColor 81 | horizontalAlignment: Text.AlignHCenter 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /qml/pages/ChannelPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../js/logic/users.js" as UsersLogic 4 | 5 | Page { 6 | id: channelPage 7 | allowedOrientations: Orientation.All 8 | 9 | // properties from lower stack page 10 | property int channelIndex 11 | property string type 12 | // 13 | 14 | property variant channel 15 | property variant messages 16 | property int messageLen: messages.len 17 | 18 | onMessageLenChanged: { 19 | console.log('changed!', messageLen, messagesList.model.count) 20 | refreshMessages() 21 | } 22 | 23 | function refreshMessages () { 24 | if (!channel || !messagesList.model.count) { 25 | return 26 | } 27 | 28 | if (messageLen === -1) { 29 | console.log('no new messages, but refreshing') 30 | loadMessages() 31 | return 32 | } 33 | 34 | var msg = messagesModel.getLatest(channel.id) 35 | console.log(msg, JSON.stringify(msg)) 36 | if (msg && msg.channel) { 37 | messagesList.model.append(msg) 38 | } 39 | } 40 | 41 | function loadMessages () { 42 | console.log('loadMessages') 43 | messagesList.model.clear() 44 | console.log('cleared') 45 | 46 | console.log('channel.id', JSON.stringify(channel)) 47 | var messages = messagesModel.getAll(channel.id) 48 | appendMessagesToModel(messages) 49 | } 50 | 51 | function loadChannelHistory () { 52 | var msg = messagesList && messagesList.model.get(0) 53 | var timestamp = msg && msg.timestamp || '' 54 | var messagesJson = messagesModel.getAllWithHistory(type, channel.id, timestamp) 55 | if (messagesJson.length < 3) { 56 | return 57 | } 58 | messagesList.model.clear() 59 | appendMessagesToModel(messagesJson) 60 | } 61 | 62 | function appendMessagesToModel (messages) { 63 | // go binding returns null as string 64 | if (messages === 'null') { 65 | return 66 | } 67 | messages = JSON.parse(messages) 68 | console.log(messages) 69 | console.log(JSON.stringify(messages)) 70 | messages.forEach(function (m) { 71 | messagesList.model.append(m) 72 | }) 73 | } 74 | 75 | 76 | Component.onCompleted: { 77 | console.log(channelIndex, type) 78 | switch (type) { 79 | case 'channel': 80 | channel = channelsModel.get(channelIndex) 81 | break 82 | case 'im': 83 | channel = imChannelsModel.getChannel(channelIndex) 84 | console.log(channel) 85 | break 86 | } 87 | 88 | messages = messagesModel 89 | 90 | loadMessages() 91 | if (messagesList.model.count === 0) { 92 | loadChannelHistory() 93 | } 94 | messagesList.positionViewAtEnd() 95 | } 96 | 97 | Component.onDestruction: { 98 | if (type === 'im') { 99 | imChannelsModel.close() 100 | } 101 | } 102 | 103 | 104 | SilicaListView { 105 | id: messagesList 106 | model: ListModel{} 107 | anchors.fill: parent 108 | 109 | PullDownMenu { 110 | MenuItem { 111 | text: "load more messages" 112 | onClicked: loadChannelHistory() 113 | } 114 | } 115 | 116 | header: Column { 117 | width: parent.width - Theme.paddingLarge 118 | x: Theme.paddingLarge 119 | 120 | PageHeader { 121 | title: channelPage.channel.name 122 | } 123 | 124 | Label { 125 | width: parent.width 126 | wrapMode: TextEdit.WordWrap 127 | textFormat: Text.RichText 128 | font.pixelSize: Theme.fontSizeSmall 129 | text: channelPage.channel.purpose.value 130 | color: Theme.secondaryColor 131 | } 132 | 133 | Rectangle { 134 | color: "transparent" 135 | width: parent.width 136 | height: Theme.paddingMedium 137 | } 138 | } 139 | 140 | 141 | 142 | delegate: ListItem { 143 | contentHeight: column.height 144 | 145 | Column { 146 | id: column 147 | width: parent.width - Theme.paddingLarge 148 | anchors.verticalCenter: parent.verticalCenter 149 | x: Theme.paddingLarge 150 | 151 | Label { 152 | anchors.left: parent.left 153 | width: parent.width 154 | wrapMode: TextEdit.WordWrap 155 | text: model.text 156 | textFormat: Text.RichText 157 | font.pixelSize: Theme.fontSizeSmall 158 | color: model.processing ? Theme.secondaryColor : Theme.primaryColor 159 | onLinkActivated: UsersLogic.handleLink(link) 160 | } 161 | 162 | SectionHeader { 163 | anchors.right: parent.right 164 | width: parent.width 165 | color: Theme.secondaryColor 166 | text: { 167 | var params = encodeURIComponent(JSON.stringify({id: model.user, index: channelIndex})) 168 | return '' + UsersLogic.get([model.user]).name + ' ' + new Date(model.timestamp * 1000).toLocaleString(null, Locale.ShortFormat) + '' // TODO: styling is awful! 169 | } 170 | onLinkActivated: UsersLogic.handleLink(link) 171 | } 172 | } 173 | } 174 | 175 | footer: TextArea { 176 | id: textAreaMessage 177 | width: parent.width 178 | placeholderText: qsTr("Message " + '#' + channelPage.channel.name) 179 | 180 | EnterKey.enabled: text.length > 0 181 | EnterKey.iconSource: "image://theme/icon-m-enter-accept" 182 | EnterKey.onClicked: { 183 | channelPage.messages.sendMessage(channelPage.channel.id, text) 184 | text = "" 185 | } 186 | } 187 | // TODO: enable once sailfish uses Qt >= 5.4 188 | // footerPositioning: ListView.OverlayFooter 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /qml/pages/Image.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | 4 | Page { 5 | id: page 6 | 7 | property variant model 8 | 9 | ProgressBar { 10 | width: parent.width 11 | anchors.centerIn: parent 12 | minimumValue: 0 13 | maximumValue: 1 14 | valueText: parseInt(value * 100) + "%" 15 | value: image.progress 16 | visible: image.status === Image.Loading 17 | } 18 | 19 | SilicaFlickable { 20 | anchors.fill: parent 21 | width: parent.width 22 | contentHeight: column.height 23 | 24 | Column { 25 | id: column 26 | width: parent.width 27 | 28 | PageHeader { title: model.name } 29 | 30 | Image { 31 | id: image 32 | visible: status === Image.Ready 33 | source: model.source 34 | width: parent.width 35 | fillMode: Image.PreserveAspectFit 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /qml/pages/Profile.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import Sailfish.Silica 1.0 3 | import "../js/logic/users.js" as UsersLogic 4 | 5 | Page{ 6 | id: profilePage 7 | allowedOrientations: Orientation.All 8 | 9 | // properties from lower stack page 10 | property string id 11 | property int index 12 | // 13 | property variant user 14 | property variant model 15 | 16 | Component.onCompleted: { 17 | user = UsersLogic.get(id) 18 | console.log(JSON.stringify(user)) 19 | 20 | model = { 21 | source: user.profile.imageOriginal || user.profile.image192, 22 | name: user.name 23 | } 24 | } 25 | 26 | SilicaFlickable { 27 | anchors.fill: parent 28 | contentHeight: content.height 29 | 30 | Column { 31 | id: content 32 | anchors { 33 | left: parent.left 34 | right: parent.right 35 | margins: Theme.paddingLarge 36 | } 37 | spacing: Theme.paddingMedium 38 | 39 | PageHeader { 40 | title: user.name 41 | width: parent.width 42 | } 43 | 44 | Column { 45 | id: portrait 46 | width: parent.width 47 | 48 | Image { 49 | anchors.horizontalCenter: parent.horizontalCenter 50 | 51 | asynchronous : true 52 | fillMode : Image.PreserveAspectFit 53 | source: profilePage.model.source 54 | 55 | MouseArea { 56 | anchors.fill: parent 57 | onClicked: pageStack.push(Qt.resolvedUrl("Image.qml"), {model: profilePage.model}) 58 | } 59 | } 60 | 61 | SectionHeader { 62 | text: qsTr('Real Name') 63 | } 64 | Label { 65 | text: user.realName 66 | anchors.horizontalCenter: parent.horizontalCenter 67 | } 68 | 69 | SectionHeader { 70 | text: qsTr('Details') 71 | } 72 | 73 | DetailItem { 74 | label: qsTr('2FA activated') 75 | value: user.has2FA 76 | } 77 | DetailItem { 78 | label: qsTr('Admin') 79 | value: user.isAdmin 80 | } 81 | DetailItem { 82 | label: qsTr('Owner') 83 | value: user.isOwner 84 | } 85 | DetailItem { 86 | label: qsTr('Restricted') 87 | value: user.isRestricted 88 | } 89 | } 90 | 91 | Button { 92 | anchors.horizontalCenter: parent.horizontalCenter 93 | onClicked: pageStack.push(Qt.resolvedUrl("ChannelPage.qml"), { channelIndex: index, type: 'im'}) 94 | text: qsTr('Send message') 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /rpm/harbour-slackfish.changes.in: -------------------------------------------------------------------------------- 1 | # Rename this file as harbour-slackfish.changes to include changelog 2 | # entries in your RPM file. 3 | # 4 | # Add new changelog entries following the format below. 5 | # Add newest entries to the top of the list. 6 | # Separate entries from eachother with a blank line. 7 | 8 | # * date Author's Name version-release 9 | # - Summary of changes 10 | 11 | * Sun Apr 13 2014 Jack Tar 0.0.1-1 12 | - Scrubbed the deck 13 | - Hoisted the sails 14 | 15 | 16 | -------------------------------------------------------------------------------- /rpm/harbour-slackfish.spec: -------------------------------------------------------------------------------- 1 | # 2 | # Do NOT Edit the Auto-generated Part! 3 | # Generated by: spectacle version 0.27 4 | # 5 | 6 | Name: harbour-slackfish 7 | 8 | # >> macros 9 | # << macros 10 | 11 | %{!?qtc_qmake:%define qtc_qmake %qmake} 12 | %{!?qtc_qmake5:%define qtc_qmake5 %qmake5} 13 | %{!?qtc_make:%define qtc_make make} 14 | %{?qtc_builddir:%define _builddir %qtc_builddir} 15 | Summary: Slackfish 16 | Version: 1.0 17 | Release: 1 18 | Group: Qt/Qt 19 | License: LICENSE 20 | URL: http://example.org/ 21 | Source0: %{name}-%{version}.tar.bz2 22 | Source100: harbour-slackfish.yaml 23 | Requires: sailfishsilica-qt5 >= 0.10.9 24 | BuildRequires: pkgconfig(sailfishapp) >= 1.0.2 25 | BuildRequires: pkgconfig(Qt5Core) 26 | BuildRequires: pkgconfig(Qt5Qml) 27 | BuildRequires: pkgconfig(Qt5Quick) 28 | BuildRequires: desktop-file-utils 29 | 30 | %description 31 | Short description of my SailfishOS Application 32 | 33 | 34 | %prep 35 | # >> setup 36 | #%setup -q -n example-app-%{version} 37 | rm -rf vendor 38 | # << setup 39 | 40 | %build 41 | # >> build pre 42 | GOPATH=%(pwd):~/ 43 | GOROOT=~/go 44 | export GOPATH GOROOT 45 | cd %(pwd) 46 | if [ $DEB_HOST_ARCH == "armel" ] 47 | then 48 | ~/go/bin/linux_arm/go build -ldflags "-s" -o %{name} 49 | else 50 | ~/go/bin/go build -ldflags "-s" -o %{name} 51 | fi 52 | # << build pre 53 | 54 | # >> build post 55 | # << build post 56 | 57 | %install 58 | rm -rf %{buildroot} 59 | # >> install pre 60 | # << install pre 61 | install -d %{buildroot}%{_bindir} 62 | install -p -m 0755 %(pwd)/%{name} %{buildroot}%{_bindir}/%{name} 63 | install -d %{buildroot}%{_datadir}/applications 64 | install -d %{buildroot}%{_datadir}/%{name}/qml 65 | install -d %{buildroot}%{_datadir}/%{name}/qml/pages 66 | install -d %{buildroot}%{_datadir}/%{name}/qml/cover 67 | install -d %{buildroot}%{_datadir}/%{name}/qml/images 68 | install -d %{buildroot}%{_datadir}/%{name}/qml/js 69 | install -d %{buildroot}%{_datadir}/%{name}/qml/js/logic 70 | install -d %{buildroot}%{_datadir}/%{name}/qml/js/services 71 | install -d %{buildroot}%{_datadir}/%{name}/qml/i18n 72 | install -m 0444 -t %{buildroot}%{_datadir}/%{name}/qml qml/*.qml 73 | install -m 0444 -t %{buildroot}%{_datadir}/%{name}/qml/pages qml/pages/*.qml 74 | install -m 0444 -t %{buildroot}%{_datadir}/%{name}/qml/images qml/images/*.png 75 | install -m 0444 -t %{buildroot}%{_datadir}/%{name}/qml/js/logic qml/js/logic/*.js 76 | install -m 0444 -t %{buildroot}%{_datadir}/%{name}/qml/js/services qml/js/services/*.js 77 | #install -m 0444 -t %{buildroot}%{_datadir}/%{name}/qml/i18n i18n/*.qm 78 | install -d %{buildroot}%{_datadir}/icons/hicolor/86x86/apps 79 | install -m 0444 -t %{buildroot}%{_datadir}/icons/hicolor/86x86/apps data/%{name}.png 80 | install -p %(pwd)/harbour-slackfish.desktop %{buildroot}%{_datadir}/applications/%{name}.desktop 81 | # >> install post 82 | # << install post 83 | 84 | desktop-file-install --delete-original \ 85 | --dir %{buildroot}%{_datadir}/applications \ 86 | %{buildroot}%{_datadir}/applications/*.desktop 87 | 88 | %files 89 | %defattr(-,root,root,-) 90 | %{_datadir}/applications/%{name}.desktop 91 | %{_datadir}/%{name}/qml 92 | %{_datadir}/%{name}/qml/i18n 93 | %{_datadir}/icons/hicolor/86x86/apps 94 | %{_bindir} 95 | # >> files 96 | # << files 97 | -------------------------------------------------------------------------------- /settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | // Settings serializes to a JSON file at choosen Location 9 | type Settings struct { 10 | Token string 11 | Location string 12 | } 13 | 14 | // Save saves Settings to Location 15 | func (s *Settings) Save() error { 16 | data, err := json.Marshal(s) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return ioutil.WriteFile(s.Location, data, 0600) 22 | } 23 | 24 | // Load loads from Location 25 | func (s *Settings) Load() error { 26 | data, err := ioutil.ReadFile(s.Location) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | err = json.Unmarshal(data, s) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /slack/channels.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import qml "gopkg.in/qml.v1" 4 | import slackApi "github.com/nlopes/slack" 5 | 6 | type Channels struct { 7 | list []Channel 8 | Len int 9 | } 10 | 11 | type Channel struct { 12 | ID string 13 | Name string 14 | Created string 15 | Creator string 16 | IsArchived bool 17 | IsGeneral bool 18 | IsMember bool 19 | IsStarred bool 20 | // Members array 21 | Topic Topic 22 | Purpose Purpose 23 | LastRead string 24 | // Latest object 25 | UnreadCount int 26 | UnreadCountDisplay int 27 | IsIM bool 28 | } 29 | 30 | type Topic struct { 31 | Value string 32 | Creator string 33 | LastSet string 34 | } 35 | 36 | type Purpose struct { 37 | Value string 38 | Creator string 39 | LastSet string 40 | } 41 | 42 | func (p *Purpose) transformFromBackend(purpose slackApi.Purpose) { 43 | p.Value = purpose.Value 44 | p.Creator = purpose.Creator 45 | p.LastSet = purpose.LastSet.String() 46 | } 47 | 48 | func (t *Topic) transformFromBackend(topic slackApi.Topic) { 49 | t.Value = topic.Value 50 | t.Creator = topic.Creator 51 | t.LastSet = topic.LastSet.String() 52 | } 53 | 54 | func (c *Channel) transformFromBackend(channel *slackApi.Channel) { 55 | infoLn("#########################", channel) 56 | t := Topic{} 57 | t.transformFromBackend(channel.Topic) 58 | p := Purpose{} 59 | p.transformFromBackend(channel.Purpose) 60 | 61 | c.ID = channel.ID 62 | c.Name = channel.Name 63 | c.Created = channel.Created.String() 64 | c.Creator = channel.Creator 65 | c.IsArchived = channel.IsArchived 66 | c.IsGeneral = channel.IsGeneral 67 | c.IsMember = channel.IsMember 68 | // Members array, 69 | c.Topic = t 70 | c.Purpose = p 71 | c.LastRead = channel.LastRead 72 | // Latest object, 73 | c.UnreadCount = channel.UnreadCount 74 | c.UnreadCountDisplay = channel.UnreadCountDisplay 75 | } 76 | 77 | func (cs *Channels) Get(i int) Channel { 78 | infoLn("Channel.Get", cs.list[i]) 79 | return cs.list[i] 80 | } 81 | 82 | // GetByID returns a Channel by id 83 | func (cs *Channels) GetByID(channelID string, userID string) Channel { 84 | infoLn(channelID) 85 | 86 | var channel Channel 87 | for _, c := range cs.list { 88 | if c.ID == channelID { 89 | channel = c 90 | } 91 | } 92 | 93 | // Must be a brand new channel 94 | if channel.ID == "" { 95 | channel.ID = channelID 96 | channel.Name = Slack.Users.getInternal(userID)[userID].Name 97 | channel.IsIM = true 98 | } 99 | 100 | return channel 101 | } 102 | 103 | func (cs *Channels) GetChannels(excludeArchived bool) { 104 | channels, err := API.GetChannels(excludeArchived) 105 | if err != nil { 106 | errorLn(err.Error()) 107 | return 108 | } 109 | 110 | for _, channel := range channels { 111 | infoLn(channel) 112 | c := Channel{} 113 | c.transformFromBackend(&channel) 114 | 115 | cs.list = append(cs.list, c) 116 | } 117 | cs.Len = len(cs.list) 118 | 119 | qml.Changed(cs, &cs.Len) 120 | } 121 | 122 | func (cs *Channels) AddChannels(channels []slackApi.Channel) { 123 | for _, channel := range channels { 124 | c := Channel{} 125 | c.transformFromBackend(&channel) 126 | cs.list = append(cs.list, c) 127 | } 128 | cs.Len = len(cs.list) 129 | qml.Changed(cs, &cs.Len) 130 | } 131 | 132 | func (cs *Channels) addChannel(c Channel) int { 133 | cs.list = append(cs.list, c) 134 | cs.Len = len(cs.list) 135 | qml.Changed(cs, &cs.Len) 136 | 137 | return cs.Len - 1 138 | } 139 | -------------------------------------------------------------------------------- /slack/imChannels.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | slackApi "github.com/nlopes/slack" 5 | qml "gopkg.in/qml.v1" 6 | ) 7 | 8 | var channelID string 9 | 10 | // IMs is a list of IM 11 | type IMs struct { 12 | list []IM 13 | Len int 14 | } 15 | 16 | // IM describes an instantMessageChannel 17 | type IM struct { 18 | IsIM bool `json:"isIm"` 19 | User string `json:"user"` 20 | IsUserDeleted bool `json:"isUserDeleted"` 21 | } 22 | 23 | // AddIMs parses IM channels from backend and saves them 24 | func (ims *IMs) AddIMs(instantMessageChannels []slackApi.IM) { 25 | infoLn("instantMessageChannels", instantMessageChannels) 26 | for _, instantMessageChannel := range instantMessageChannels { 27 | im := IM{} 28 | im.transformFromBackend(&instantMessageChannel) 29 | ims.list = append(ims.list, im) 30 | } 31 | 32 | ims.Len = len(ims.list) 33 | qml.Changed(ims, &ims.Len) 34 | } 35 | 36 | func (im *IM) transformFromBackend(instantMessageChannel *slackApi.IM) { 37 | im.IsIM = instantMessageChannel.IsIM 38 | im.User = instantMessageChannel.User 39 | im.IsUserDeleted = instantMessageChannel.IsUserDeleted 40 | } 41 | 42 | // Open opens an channel based on a userID and sets the current channel 43 | func (ims *IMs) Open(userID string) { 44 | _, _, chID, err := API.OpenIMChannel(userID) 45 | if err != nil { 46 | errorLn(err.Error()) 47 | } 48 | 49 | channelID = chID 50 | } 51 | 52 | // GetIMs returns all IM-channels 53 | func (ims *IMs) GetIMs() { 54 | imChannels, err := API.GetIMChannels() 55 | if err != nil { 56 | errorLn(err.Error()) 57 | return 58 | } 59 | 60 | for _, channel := range imChannels { 61 | infoLn(channel) 62 | c := IM{} 63 | c.transformFromBackend(&channel) 64 | 65 | ims.list = append(ims.list, c) 66 | } 67 | ims.Len = len(ims.list) 68 | 69 | qml.Changed(ims, &ims.Len) 70 | } 71 | 72 | // Close closes the currently open channel 73 | func (ims *IMs) Close() { 74 | _, _, err := API.CloseIMChannel(channelID) 75 | if err != nil { 76 | errorLn(err.Error()) 77 | } 78 | 79 | channelID = "" 80 | } 81 | 82 | // Get returns an IM 83 | func (ims *IMs) Get(i int) IM { 84 | infoLn(ims.list[i]) 85 | return ims.list[i] 86 | } 87 | 88 | // GetChannel returns a channel informations of an IM 89 | func (ims *IMs) GetChannel(i int) Channel { 90 | infoLn(ims.list[i]) 91 | userID := ims.list[i].User 92 | _, _, channelID, err := API.OpenIMChannel(userID) 93 | if err != nil { 94 | errorLn(err) 95 | } 96 | infoLn(channelID) 97 | return Slack.Channels.GetByID(channelID, userID) 98 | } 99 | -------------------------------------------------------------------------------- /slack/logger.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var logger *log.Logger 9 | 10 | func init() { 11 | logger = log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags) 12 | } 13 | 14 | func errorLn(v ...interface{}) { 15 | log.Printf("ERROR: %+v\n", v) 16 | } 17 | 18 | func warnLn(v ...interface{}) { 19 | log.Printf("WARN: %+v\n", v) 20 | } 21 | 22 | func infoLn(v ...interface{}) { 23 | log.Printf("INFO: %+v\n", v) 24 | } 25 | -------------------------------------------------------------------------------- /slack/messages.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import qml "gopkg.in/qml.v1" 4 | import slackApi "github.com/nlopes/slack" 5 | import "encoding/json" 6 | 7 | type Messages struct { 8 | list []Message 9 | Len int 10 | } 11 | 12 | type Message struct { 13 | Type string `json:"type"` 14 | Channel string `json:"channel"` 15 | User string `json:"user"` 16 | Text string `json:"text"` 17 | Timestamp string `json:"timestamp"` 18 | IsStarred bool `json:"isStarred"` 19 | ID int 20 | Processing bool `json:"processing"` 21 | // PinnedTo []string 22 | // Attachments []Attachment 23 | // Edited *Edited 24 | } 25 | 26 | func (m *Message) transformFromBackend(msg *slackApi.Msg) { 27 | m.Type = msg.Type 28 | m.Channel = msg.Channel 29 | m.User = msg.User 30 | m.Text = msg.Text 31 | m.Timestamp = msg.Timestamp 32 | m.IsStarred = msg.IsStarred 33 | } 34 | 35 | // GetLatest returns the latest message for given channel 36 | func (ms *Messages) GetLatest(channelID string) Message { 37 | m := ms.list[len(ms.list)-1] 38 | if m.Channel == channelID { 39 | return m 40 | } 41 | return Message{} 42 | } 43 | 44 | func (ms *Messages) GetAll(channelID string) string { 45 | var chMsg []Message 46 | for _, m := range ms.list { 47 | if m.Channel == channelID { 48 | infoLn("GetAll: Adding this messages", m) 49 | chMsg = append(chMsg, m) 50 | } 51 | } 52 | s, _ := json.Marshal(chMsg) 53 | return string(s) 54 | } 55 | 56 | func (ms *Messages) GetAllWithHistory(channelType string, channelID string, timestamp string) string { 57 | params := slackApi.HistoryParameters{ 58 | Count: 30, 59 | Inclusive: true, 60 | } 61 | if timestamp != "" { 62 | params.Latest = timestamp 63 | } 64 | 65 | var err error 66 | var h *slackApi.History 67 | if channelType == "channel" { 68 | h, err = API.GetChannelHistory(channelID, params) 69 | } else if channelType == "im" { 70 | h, err = API.GetIMHistory(channelID, params) 71 | } 72 | if err != nil { 73 | errorLn(err.Error()) 74 | return "" 75 | } 76 | 77 | infoLn(h.Messages) 78 | 79 | var tmpMs []Message 80 | for i := len(h.Messages) - 1; i > 0; i-- { 81 | msg := Message{} 82 | msg.transformFromBackend(&h.Messages[i].Msg) 83 | msg.Channel = channelID 84 | tmpMs = append(tmpMs, msg) 85 | } 86 | ms.list = append(tmpMs, ms.list...) 87 | ms.Len = len(ms.list) 88 | 89 | return ms.GetAll(channelID) 90 | } 91 | 92 | func (ms *Messages) Add(msg *slackApi.Msg) { 93 | m := Message{} 94 | m.transformFromBackend(msg) 95 | 96 | ms.add(m) 97 | } 98 | 99 | func (ms *Messages) add(m Message) { 100 | infoLn("add", m) 101 | ms.list = append(ms.list, m) 102 | infoLn(ms.Len) 103 | ms.Len = len(ms.list) 104 | infoLn(ms.Len) 105 | 106 | qml.Changed(ms, &ms.Len) 107 | } 108 | 109 | func (ms *Messages) SendMessage(channelID string, text string) { 110 | outgoingMsg := slackRtm.NewOutgoingMessage(text, channelID) 111 | slackRtm.SendMessage(outgoingMsg) 112 | 113 | ms.add(Message{ID: outgoingMsg.ID, Text: text, Channel: channelID, Type: "message", User: "TODO me", Timestamp: "TODO now", Processing: true}) 114 | } 115 | 116 | func (ms *Messages) MarkSent(ID int) { 117 | for i := ms.Len - 1; i > 0; i-- { 118 | if ms.list[i].ID != ID { 119 | continue 120 | } 121 | 122 | ms.list[i].Processing = false 123 | 124 | tmp := ms.Len 125 | ms.Len = -1 126 | qml.Changed(ms, &ms.Len) 127 | ms.Len = tmp 128 | return 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "fmt" 5 | 6 | slackApi "github.com/nlopes/slack" 7 | ) 8 | 9 | // API exports API of slack package 10 | var API *slackApi.Client 11 | var slackRtm *slackApi.RTM 12 | var Slack = Model{} 13 | 14 | var messageID = 0 15 | var token string 16 | 17 | // SlackModel represents the entity models for storing information by @API 18 | type Model struct { 19 | Messages Messages 20 | Users Users 21 | Channels Channels 22 | IMs IMs 23 | } 24 | 25 | // Connect establishes a connection to slack API 26 | func (s *Model) Connect(tkn string) { 27 | token = tkn 28 | API = slackApi.New(tkn) 29 | 30 | slackApi.SetLogger(logger) 31 | API.SetDebug(true) 32 | 33 | slackRtm = API.NewRTM() 34 | info, _, _ := slackRtm.StartRTM() 35 | s.Users.AddUsers(info.Users) 36 | s.Channels.AddChannels(info.Channels) 37 | s.IMs.AddIMs(info.IMs) 38 | 39 | go slackRtm.ManageConnection() 40 | 41 | go processEvents(s) 42 | } 43 | 44 | // Disconnect from slack API 45 | func (s *Model) Disconnect() { 46 | err := slackRtm.Disconnect() 47 | if err != nil { 48 | errorLn(err.Error()) 49 | } 50 | } 51 | 52 | func processEvents(s *Model) { 53 | for { 54 | select { 55 | case msg := <-slackRtm.IncomingEvents: 56 | fmt.Print("Event Received: ") 57 | switch ev := msg.Data.(type) { 58 | case *slackApi.HelloEvent: 59 | // Ignore hello 60 | 61 | case *slackApi.ConnectedEvent: 62 | fmt.Printf("Infos: %+v \n", ev.Info) 63 | fmt.Println("Connection counter:", ev.ConnectionCount) 64 | 65 | case *slackApi.MessageEvent: 66 | fmt.Printf("Message: %v\n", ev) 67 | s.Messages.Add(&ev.Msg) 68 | 69 | case *slackApi.PresenceChangeEvent: 70 | fmt.Printf("Presence Change: %v\n", ev) 71 | 72 | case *slackApi.LatencyReport: 73 | fmt.Printf("Current latency: %v\n", ev.Value) 74 | 75 | case *slackApi.RTMError: 76 | fmt.Printf("Error: %s\n", ev.Error()) 77 | 78 | case *slackApi.InvalidAuthEvent: 79 | fmt.Printf("Invalid credentials") 80 | break 81 | 82 | case *slackApi.AckMessage: 83 | fmt.Printf("AckMessage: %+v\n", ev) 84 | s.Messages.MarkSent(ev.ReplyTo) 85 | 86 | default: 87 | 88 | fmt.Printf("Unexpected (%+v): %v\n", ev, msg.Data) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /slack/users.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | qml "gopkg.in/qml.v1" 7 | 8 | slackApi "github.com/nlopes/slack" 9 | ) 10 | 11 | // Users holding collection of all users 12 | type Users struct { 13 | users []User 14 | Len int 15 | } 16 | 17 | // User is a Slack user accounts 18 | type User struct { 19 | Profile UserProfile `json:"profile"` 20 | ID string `json:"id"` 21 | Name string `json:"name"` 22 | Deleted bool `json:"deleted"` 23 | Color string `json:"color"` 24 | RealName string `json:"realName"` 25 | TZ string `json:"tz"` 26 | TZLabel string `json:"tzLabel"` 27 | TZOffset int `json:"tzOffset"` 28 | IsBot bool `json:"isBot"` 29 | IsAdmin bool `json:"isAdmin"` 30 | IsOwner bool `json:"isOwner"` 31 | IsPrimaryOwner bool `json:"isPrimaryOwner"` 32 | IsRestricted bool `json:"isRestricted"` 33 | IsUltraRestricted bool `json:"isUltraRestricted"` 34 | Has2FA bool `json:"has2FA"` 35 | HasFiles bool `json:"hasFiles"` 36 | Presence string `json:"presence"` 37 | } 38 | 39 | // UserProfile holds the personal information of a @User 40 | type UserProfile struct { 41 | FirstName string `json:"firstName"` 42 | LastName string `json:"lastName"` 43 | RealName string `json:"realName"` 44 | RealNameNormalized string `json:"realNameNormalized"` 45 | Email string `json:"email"` 46 | Skype string `json:"skype"` 47 | Phone string `json:"phone"` 48 | Image24 string `json:"image24"` 49 | Image32 string `json:"image32"` 50 | Image48 string `json:"image48"` 51 | Image72 string `json:"image72"` 52 | Image192 string `json:"image192"` 53 | ImageOriginal string `json:"imageOriginal"` 54 | Title string `json:"title"` 55 | BotID string `json:"botId,omitempty"` 56 | APIAppID string `json:"apiAppId,omitempty"` 57 | } 58 | 59 | func (up *UserProfile) transformFromBack(userProfile *slackApi.UserProfile) { 60 | up.FirstName = userProfile.FirstName 61 | up.LastName = userProfile.LastName 62 | up.RealName = userProfile.RealName 63 | up.RealNameNormalized = userProfile.RealNameNormalized 64 | up.Email = userProfile.Email 65 | up.Skype = userProfile.Skype 66 | up.Phone = userProfile.Phone 67 | up.Image24 = userProfile.Image24 68 | up.Image32 = userProfile.Image32 69 | up.Image48 = userProfile.Image48 70 | up.Image72 = userProfile.Image72 71 | up.Image192 = userProfile.Image192 72 | up.ImageOriginal = userProfile.ImageOriginal 73 | up.Title = userProfile.Title 74 | // up.BotID = userProfile.BotID // FIXME: not defined on *slackApi.UserProfile 75 | // up.APIAppID = userProfile.ApiAppID // FIXME: not defined on *slackApi.UserProfile 76 | } 77 | 78 | func (u *User) transformFromBackend(user *slackApi.User) { 79 | up := UserProfile{} 80 | up.transformFromBack(&user.Profile) 81 | u.Profile = up 82 | 83 | u.ID = user.ID 84 | u.Name = user.Name 85 | u.Deleted = user.Deleted 86 | u.Color = user.Color 87 | u.RealName = user.RealName 88 | u.TZ = user.TZ 89 | u.TZLabel = user.TZLabel 90 | u.TZOffset = user.TZOffset 91 | u.IsBot = user.IsBot 92 | u.IsAdmin = user.IsAdmin 93 | u.IsOwner = user.IsOwner 94 | u.IsPrimaryOwner = user.IsPrimaryOwner 95 | u.IsRestricted = user.IsRestricted 96 | u.IsUltraRestricted = user.IsUltraRestricted 97 | u.Has2FA = user.Has2FA 98 | u.HasFiles = user.HasFiles 99 | u.Presence = user.Presence 100 | } 101 | 102 | func (us *Users) getUsers(ID string) (users map[string]User) { 103 | users = map[string]User{} 104 | if ID != "" { 105 | for _, user := range us.users { 106 | if user.ID != ID { 107 | continue 108 | } 109 | 110 | users[user.ID] = user 111 | } 112 | } else { 113 | for _, user := range us.users { 114 | users[user.ID] = user 115 | } 116 | } 117 | 118 | return users 119 | } 120 | 121 | // Get returns a all (or a single user) as JSON string 122 | func (us *Users) Get(ID string) string { 123 | users := us.getUsers(ID) 124 | s, _ := json.Marshal(users) 125 | infoLn(string(s)) 126 | return string(s) 127 | } 128 | 129 | func (us *Users) getInternal(ID string) map[string]User { 130 | return us.getUsers(ID) 131 | } 132 | 133 | // AddUsers converts users from backend 134 | func (us *Users) AddUsers(users []slackApi.User) { 135 | for _, user := range users { 136 | u := User{} 137 | u.transformFromBackend(&user) 138 | us.users = append(us.users, u) 139 | } 140 | 141 | us.Len = len(us.users) 142 | qml.Changed(us, &us.Len) 143 | } 144 | -------------------------------------------------------------------------------- /translations/harbour-slackfish-de.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ChannelPage 6 | 7 | Channels 8 | 9 | 10 | 11 | 12 | CoverPage 13 | 14 | My Cover 15 | Mein Cover 16 | 17 | 18 | 19 | FirstPage 20 | 21 | Show Page 2 22 | 23 | 24 | 25 | UI Template 26 | 27 | 28 | 29 | Hello Sailors 30 | 31 | 32 | 33 | 34 | SecondPage 35 | 36 | Nested Page 37 | Unterseite 38 | 39 | 40 | Item 41 | Element 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /translations/harbour-slackfish.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ChannelPage 6 | 7 | Channels 8 | 9 | 10 | 11 | 12 | CoverPage 13 | 14 | My Cover 15 | 16 | 17 | 18 | 19 | FirstPage 20 | 21 | Show Page 2 22 | 23 | 24 | 25 | UI Template 26 | 27 | 28 | 29 | Hello Sailors 30 | 31 | 32 | 33 | 34 | SecondPage 35 | 36 | Nested Page 37 | 38 | 39 | 40 | Item 41 | 42 | 43 | 44 | 45 | --------------------------------------------------------------------------------